From 58fdad5a79401d20eec488dd275a577a978be58a Mon Sep 17 00:00:00 2001 From: ghostcoder42 Date: Sat, 5 Sep 2026 15:29:51 +0800 Subject: [PATCH] feat: autoplay on entering the watch screen, on by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Playback section to Settings with an autoplay toggle (MMKV-backed, defaults to enabled) rendered as a Switch with the app's rose track color — the switch itself is the tap target, so a press can't double-toggle through a row-level pressable. The watch screen starts playback when it is entered or the source changes (new video via the stack, or a resolution switch); returning from a deeper screen stays paused since the focus cleanup already paused it on leave. --- src/app/(app)/settings.tsx | 22 ++++++++++++++++++++-- src/app/watch/[id].tsx | 18 +++++++++++++++++- src/lib/hooks/index.ts | 1 + src/lib/hooks/use-playback-settings.ts | 16 ++++++++++++++++ src/translations/en.json | 3 +++ src/translations/ja.json | 3 +++ src/translations/ko.json | 3 +++ src/translations/zh-CN.json | 3 +++ src/translations/zh.json | 3 +++ 9 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/lib/hooks/use-playback-settings.ts diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx index 1ce03fe..32def8a 100644 --- a/src/app/(app)/settings.tsx +++ b/src/app/(app)/settings.tsx @@ -4,7 +4,7 @@ import { Icon } from '@/components/icon'; import { SafeAreaView } from '@/components/safe-area-view'; import { UpdateDialog } from '@/components/update-dialog'; import { SITE_DOMAINS, SITE_DOMAIN_KEY, type SiteDomain } from '@/lib/hanime1/endpoints'; -import { type ThemeMode, useThemeConfig } from '@/lib/hooks'; +import { type ThemeMode, usePlaybackSettings, useThemeConfig } from '@/lib/hooks'; import { useSecuritySettings } from '@/lib/hooks/use-security-settings'; import { useUpdateCheck } from '@/lib/hooks/use-update-check'; import { LANGUAGE_LABELS, type Language } from '@/lib/i18n/resources'; @@ -16,7 +16,15 @@ import { Env } from '@env'; // bundles (binding missing at runtime), destructuring after import works. import * as ApplicationNS from 'expo-application'; import { ActivityAction, startActivityAsync } from 'expo-intent-launcher'; -import { ActivityIndicator, Platform, Pressable, ScrollView, Text, View } from 'react-native'; +import { + ActivityIndicator, + Platform, + Pressable, + ScrollView, + Switch, + Text, + View, +} from 'react-native'; import { showMessage } from 'react-native-flash-message'; import { useMMKVString } from 'react-native-mmkv'; @@ -106,6 +114,7 @@ export { ScreenErrorBoundary as ErrorBoundary }; export default function SettingsScreen() { const t = useTranslate(); const [theme, setTheme] = useThemeConfig(); + const { autoplay, setAutoplay } = usePlaybackSettings(); const [domain, setDomain] = useMMKVString(SITE_DOMAIN_KEY); const [lang, setLang] = useSelectedLanguage(); const clearHistory = useHistoryStore((s) => s.clearHistory); @@ -140,6 +149,15 @@ export default function SettingsScreen() { ))} + {t('settings.playback')} + + + + {t('settings.domain')} {SITE_DOMAINS.map((d) => ( diff --git a/src/app/watch/[id].tsx b/src/app/watch/[id].tsx index 522bad1..bab504c 100644 --- a/src/app/watch/[id].tsx +++ b/src/app/watch/[id].tsx @@ -6,7 +6,7 @@ import { SafeAreaView } from '@/components/safe-area-view'; import { toOfflineDetail } from '@/lib/download/offline-detail'; import { buildUrl, endpoints } from '@/lib/hanime1/scraper'; import { haptic, hapticSuccess } from '@/lib/haptics'; -import { useVideoDownload } from '@/lib/hooks'; +import { usePlaybackSettings, useVideoDownload } from '@/lib/hooks'; import { useTranslate } from '@/lib/i18n/utils'; import { useDownloadedStore, @@ -79,6 +79,22 @@ export default function WatchScreen() { }); const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing }); + const { autoplay } = usePlaybackSettings(); + + // Start playback automatically when the screen is entered or the source + // changes (new video via the stack, or a resolution switch) unless the + // user opted out in Settings. Coming back from a deeper screen (author / + // tag) stays paused — the focus cleanup below paused it on leave. + // biome-ignore lint/correctness/useExhaustiveDependencies: videoSource is an intentional trigger — replay when the source lands or switches. + useEffect(() => { + if (!autoplay) return; + try { + player.play(); + } catch { + // Player not attached to a source yet — replays when videoSource lands. + } + }, [autoplay, player, videoSource]); + // Pause when this screen loses focus (opening the author page, a tag page or // another video keeps this screen mounted in the stack — without this, its // audio keeps playing under the new screen). On unmount expo-video may diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index 6216465..4f18c09 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -5,6 +5,7 @@ export { PORTRAIT_MIN_TILE_WIDTH, useColumns, } from './use-columns'; +export { usePlaybackSettings } from './use-playback-settings'; export { useSearchHistory } from './use-search-history'; export { useUpdateCheck, type UpdateCheckState } from './use-update-check'; export { useVideoActions, type ActionableItem } from './use-video-actions'; diff --git a/src/lib/hooks/use-playback-settings.ts b/src/lib/hooks/use-playback-settings.ts new file mode 100644 index 0000000..1e93af7 --- /dev/null +++ b/src/lib/hooks/use-playback-settings.ts @@ -0,0 +1,16 @@ +import { useMMKVBoolean } from 'react-native-mmkv'; + +const AUTOPLAY_KEY = 'settings.autoplay'; + +/** + * Toggle for "Autoplay on entering the watch screen" — on by default. + * `undefined` (never set) reads as enabled; an explicit `false` opts out. + */ +export const usePlaybackSettings = () => { + const [autoplay, setAutoplay] = useMMKVBoolean(AUTOPLAY_KEY); + + return { + autoplay: autoplay ?? true, + setAutoplay, + }; +}; diff --git a/src/translations/en.json b/src/translations/en.json index 169c459..1e590e1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -68,6 +68,9 @@ "themeSystem": "System", "themeLight": "Light", "themeDark": "Dark", + "playback": "Playback", + "autoplay": "Autoplay", + "autoplayDesc": "Start playing automatically when you open a video", "domain": "Source domain", "domainDescription": "Switch mirror if the default is blocked or slow", "openLinks": "Open site links", diff --git a/src/translations/ja.json b/src/translations/ja.json index 85a0d1b..e16d5f9 100644 --- a/src/translations/ja.json +++ b/src/translations/ja.json @@ -68,6 +68,9 @@ "themeSystem": "システム", "themeLight": "ライト", "themeDark": "ダーク", + "playback": "再生", + "autoplay": "自動再生", + "autoplayDesc": "再生画面を開いたときに自動で再生する", "domain": "ソースドメイン", "domainDescription": "デフォルトがブロックまたは遅い場合はミラーを切り替えてください", "openLinks": "サイトリンクを開く", diff --git a/src/translations/ko.json b/src/translations/ko.json index d312dd5..18cec68 100644 --- a/src/translations/ko.json +++ b/src/translations/ko.json @@ -68,6 +68,9 @@ "themeSystem": "시스템", "themeLight": "라이트", "themeDark": "다크", + "playback": "재생", + "autoplay": "자동 재생", + "autoplayDesc": "재생 화면을 열면 자동으로 재생", "domain": "소스 도메인", "domainDescription": "기본 도메인이 차단되거나 느리면 미러를 전환하세요", "openLinks": "사이트 링크 열기", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index ff3dcec..f023982 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -68,6 +68,9 @@ "themeSystem": "跟随系统", "themeLight": "浅色", "themeDark": "深色", + "playback": "播放", + "autoplay": "自动播放", + "autoplayDesc": "进入播放页时自动开始播放", "domain": "数据源域名", "domainDescription": "若默认域名被封锁或缓慢,可切换镜像", "openLinks": "打开站点链接", diff --git a/src/translations/zh.json b/src/translations/zh.json index 5c56cdd..6492145 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -68,6 +68,9 @@ "themeSystem": "跟隨系統", "themeLight": "淺色", "themeDark": "深色", + "playback": "播放", + "autoplay": "自動播放", + "autoplayDesc": "進入播放頁時自動開始播放", "domain": "資料來源網域", "domainDescription": "若預設網域被封鎖或緩慢,可切換鏡像", "openLinks": "開啟站點連結",