feat: add video loop playback setting

Add a toggle in General Settings to enable/disable video loop playback,
following the same pattern as the existing autoplay setting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
codytseng 2026-02-18 13:55:53 +08:00
parent ae8a534103
commit 481603d0e8
23 changed files with 90 additions and 19 deletions

View file

@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
const { autoplay } = useContentPolicy()
const { autoplay, videoLoop } = useContentPolicy()
const { muteMedia, updateMuteMedia } = useUserPreferences()
const [error, setError] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
@ -79,6 +79,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
ref={videoRef}
controls
playsInline
loop={videoLoop}
className={cn('max-h-[80vh] rounded-xl border sm:max-h-[60vh]', className)}
src={src}
onClick={(e) => e.stopPropagation()}

View file

@ -24,6 +24,7 @@ export const StorageKey = {
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
AUTOPLAY: 'autoplay',
VIDEO_LOOP: 'videoLoop',
TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap',
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',

View file

@ -668,6 +668,8 @@ export default {
'No notes found': 'لم يتم العثور على ملاحظات',
'Try again later or check your connection': 'حاول مرة أخرى لاحقًا أو تحقق من اتصالك',
'Hide indirect': 'إخفاء غير المباشرة',
'Copy note content': 'نسخ محتوى الملاحظة'
'Copy note content': 'نسخ محتوى الملاحظة',
'Video loop': 'تكرار الفيديو',
'Automatically replay videos when they end': 'إعادة تشغيل مقاطع الفيديو تلقائيًا عند انتهائها'
}
}

View file

@ -692,6 +692,8 @@ export default {
'Try again later or check your connection':
'Versuchen Sie es später erneut oder überprüfen Sie Ihre Verbindung',
'Hide indirect': 'Indirekte ausblenden',
'Copy note content': 'Notizinhalt kopieren'
'Copy note content': 'Notizinhalt kopieren',
'Video loop': 'Video-Schleife',
'Automatically replay videos when they end': 'Videos automatisch wiederholen, wenn sie enden'
}
}

View file

@ -674,6 +674,8 @@ export default {
'No notes found': 'No notes found',
'Try again later or check your connection': 'Try again later or check your connection',
'Hide indirect': 'Hide indirect',
'Copy note content': 'Copy note content'
'Copy note content': 'Copy note content',
'Video loop': 'Video loop',
'Automatically replay videos when they end': 'Automatically replay videos when they end'
}
}

View file

@ -685,6 +685,8 @@ export default {
'No notes found': 'No se encontraron notas',
'Try again later or check your connection': 'Inténtalo más tarde o verifica tu conexión',
'Hide indirect': 'Ocultar indirectas',
'Copy note content': 'Copiar contenido de la nota'
'Copy note content': 'Copiar contenido de la nota',
'Video loop': 'Repetir video',
'Automatically replay videos when they end': 'Reproducir automáticamente los videos cuando terminen'
}
}

View file

@ -680,6 +680,8 @@ export default {
'Try again later or check your connection':
'بعداً دوباره امتحان کنید یا اتصال خود را بررسی کنید',
'Hide indirect': 'پنهان کردن غیرمستقیم',
'Copy note content': 'کپی محتوای یادداشت'
'Copy note content': 'کپی محتوای یادداشت',
'Video loop': 'تکرار ویدیو',
'Automatically replay videos when they end': 'پخش خودکار ویدیوها پس از پایان'
}
}

View file

@ -689,6 +689,8 @@ export default {
'No notes found': 'Aucune note trouvée',
'Try again later or check your connection': 'Réessayez plus tard ou vérifiez votre connexion',
'Hide indirect': 'Masquer indirects',
'Copy note content': 'Copier le contenu de la note'
'Copy note content': 'Copier le contenu de la note',
'Video loop': 'Boucle vidéo',
'Automatically replay videos when they end': 'Rejouer automatiquement les vidéos à la fin'
}
}

View file

@ -680,6 +680,8 @@ export default {
'No notes found': 'कोई नोट्स नहीं मिले',
'Try again later or check your connection': 'बाद में पुनः प्रयास करें या अपना कनेक्शन जाँचें',
'Hide indirect': 'अप्रत्यक्ष छुपाएं',
'Copy note content': 'नोट सामग्री कॉपी करें'
'Copy note content': 'नोट सामग्री कॉपी करें',
'Video loop': 'वीडियो लूप',
'Automatically replay videos when they end': 'वीडियो समाप्त होने पर स्वचालित रूप से दोबारा चलाएं'
}
}

View file

@ -674,6 +674,8 @@ export default {
'No notes found': 'Nem található jegyzet',
'Try again later or check your connection': 'Próbáld újra később vagy ellenőrizd a kapcsolatot',
'Hide indirect': 'Közvetettek elrejtése',
'Copy note content': 'Jegyzet tartalmának másolása'
'Copy note content': 'Jegyzet tartalmának másolása',
'Video loop': 'Videó ismétlése',
'Automatically replay videos when they end': 'Videók automatikus újrajátszása, amikor véget érnek'
}
}

View file

@ -685,6 +685,8 @@ export default {
'No notes found': 'Nessuna nota trovata',
'Try again later or check your connection': 'Riprova più tardi o controlla la connessione',
'Hide indirect': 'Nascondi indirette',
'Copy note content': 'Copia contenuto della nota'
'Copy note content': 'Copia contenuto della nota',
'Video loop': 'Ripetizione video',
'Automatically replay videos when they end': 'Riprodurre automaticamente i video quando terminano'
}
}

View file

@ -680,6 +680,8 @@ export default {
'Try again later or check your connection':
'後でもう一度お試しいただくか、接続を確認してください',
'Hide indirect': '間接通知を非表示',
'Copy note content': 'ノート内容をコピー'
'Copy note content': 'ノート内容をコピー',
'Video loop': 'ビデオループ',
'Automatically replay videos when they end': 'ビデオ終了時に自動的にリプレイする'
}
}

View file

@ -674,6 +674,8 @@ export default {
'No notes found': '노트를 찾을 수 없습니다',
'Try again later or check your connection': '나중에 다시 시도하거나 연결을 확인하세요',
'Hide indirect': '간접 숨기기',
'Copy note content': '노트 내용 복사'
'Copy note content': '노트 내용 복사',
'Video loop': '비디오 반복',
'Automatically replay videos when they end': '비디오가 끝나면 자동으로 다시 재생'
}
}

View file

@ -686,6 +686,8 @@ export default {
'No notes found': 'Nie znaleziono notatek',
'Try again later or check your connection': 'Spróbuj ponownie później lub sprawdź połączenie',
'Hide indirect': 'Ukryj pośrednie',
'Copy note content': 'Kopiuj treść notatki'
'Copy note content': 'Kopiuj treść notatki',
'Video loop': 'Zapętlanie wideo',
'Automatically replay videos when they end': 'Automatycznie powtarzaj filmy po zakończeniu'
}
}

View file

@ -683,6 +683,8 @@ export default {
'Try again later or check your connection':
'Tente novamente mais tarde ou verifique sua conexão',
'Hide indirect': 'Ocultar indiretas',
'Copy note content': 'Copiar conteúdo da nota'
'Copy note content': 'Copiar conteúdo da nota',
'Video loop': 'Repetir vídeo',
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem'
}
}

View file

@ -686,6 +686,8 @@ export default {
'Try again later or check your connection':
'Tente novamente mais tarde ou verifique a sua ligação',
'Hide indirect': 'Ocultar indiretas',
'Copy note content': 'Copiar conteúdo da nota'
'Copy note content': 'Copiar conteúdo da nota',
'Video loop': 'Repetir vídeo',
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem'
}
}

View file

@ -685,6 +685,8 @@ export default {
'No notes found': 'Заметки не найдены',
'Try again later or check your connection': 'Попробуйте позже или проверьте подключение',
'Hide indirect': 'Скрыть косвенные',
'Copy note content': 'Скопировать содержимое заметки'
'Copy note content': 'Скопировать содержимое заметки',
'Video loop': 'Зацикливание видео',
'Automatically replay videos when they end': 'Автоматически воспроизводить видео заново после окончания'
}
}

View file

@ -670,6 +670,8 @@ export default {
'No notes found': 'ไม่พบโน้ต',
'Try again later or check your connection': 'ลองใหม่ภายหลังหรือตรวจสอบการเชื่อมต่อของคุณ',
'Hide indirect': 'ซ่อนทางอ้อม',
'Copy note content': 'คัดลอกเนื้อหาโน้ต'
'Copy note content': 'คัดลอกเนื้อหาโน้ต',
'Video loop': 'เล่นวิดีโอซ้ำ',
'Automatically replay videos when they end': 'เล่นวิดีโอซ้ำอัตโนมัติเมื่อจบ'
}
}

View file

@ -652,6 +652,8 @@ export default {
'No notes found': '沒有找到筆記',
'Try again later or check your connection': '請稍後重試或檢查網路連接',
'Hide indirect': '隱藏間接通知',
'Copy note content': '複製筆記內容'
'Copy note content': '複製筆記內容',
'Video loop': '影片循環',
'Automatically replay videos when they end': '影片播放結束後自動重新播放'
}
}

View file

@ -657,6 +657,8 @@ export default {
'No notes found': '没有找到笔记',
'Try again later or check your connection': '请稍后重试或检查网络连接',
'Hide indirect': '隐藏间接通知',
'Copy note content': '复制笔记内容'
'Copy note content': '复制笔记内容',
'Video loop': '视频循环',
'Automatically replay videos when they end': '视频播放结束后自动重新播放'
}
}

View file

@ -29,6 +29,8 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const {
autoplay,
setAutoplay,
videoLoop,
setVideoLoop,
nsfwDisplayPolicy,
setNsfwDisplayPolicy,
hideContentMentioningMutedUsers,
@ -121,6 +123,15 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
</Label>
<Switch id="autoplay" checked={autoplay} onCheckedChange={setAutoplay} />
</SettingItem>
<SettingItem>
<Label htmlFor="video-loop" className="text-base font-normal">
<div>{t('Video loop')}</div>
<div className="text-muted-foreground">
{t('Automatically replay videos when they end')}
</div>
</Label>
<Switch id="video-loop" checked={videoLoop} onCheckedChange={setVideoLoop} />
</SettingItem>
<SettingItem>
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
{t('Hide content mentioning muted users')}

View file

@ -7,6 +7,9 @@ type TContentPolicyContext = {
autoplay: boolean
setAutoplay: (autoplay: boolean) => void
videoLoop: boolean
setVideoLoop: (videoLoop: boolean) => void
nsfwDisplayPolicy: TNsfwDisplayPolicy
setNsfwDisplayPolicy: (policy: TNsfwDisplayPolicy) => void
@ -40,6 +43,7 @@ export const useContentPolicy = () => {
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
const [autoplay, setAutoplay] = useState(storage.getAutoplay())
const [videoLoop, setVideoLoop] = useState(storage.getVideoLoop())
const [nsfwDisplayPolicy, setNsfwDisplayPolicy] = useState(storage.getNsfwDisplayPolicy())
const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState(
storage.getHideContentMentioningMutedUsers()
@ -94,6 +98,11 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setAutoplay(autoplay)
}
const updateVideoLoop = (videoLoop: boolean) => {
storage.setVideoLoop(videoLoop)
setVideoLoop(videoLoop)
}
const updateNsfwDisplayPolicy = (policy: TNsfwDisplayPolicy) => {
storage.setNsfwDisplayPolicy(policy)
setNsfwDisplayPolicy(policy)
@ -129,6 +138,8 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
value={{
autoplay,
setAutoplay: updateAutoplay,
videoLoop,
setVideoLoop: updateVideoLoop,
nsfwDisplayPolicy,
setNsfwDisplayPolicy: updateNsfwDisplayPolicy,
hideContentMentioningMutedUsers,

View file

@ -46,6 +46,7 @@ class LocalStorageService {
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
private videoLoop: boolean = false
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private dismissedTooManyRelaysAlert: boolean = false
@ -136,6 +137,7 @@ class LocalStorageService {
window.localStorage.getItem(StorageKey.MEDIA_UPLOAD_SERVICE) ?? DEFAULT_NIP_96_SERVICE
this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false'
this.videoLoop = window.localStorage.getItem(StorageKey.VIDEO_LOOP) === 'true'
const translationServiceConfigMapStr = window.localStorage.getItem(
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP
@ -475,6 +477,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.AUTOPLAY, autoplay.toString())
}
getVideoLoop() {
return this.videoLoop
}
setVideoLoop(videoLoop: boolean) {
this.videoLoop = videoLoop
window.localStorage.setItem(StorageKey.VIDEO_LOOP, videoLoop.toString())
}
getTranslationServiceConfig(pubkey?: string | null) {
return this.translationServiceConfigMap[pubkey ?? '_'] ?? { service: 'jumble' }
}