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:
parent
ae8a534103
commit
481603d0e8
23 changed files with 90 additions and 19 deletions
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': 'إعادة تشغيل مقاطع الفيديو تلقائيًا عند انتهائها'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': 'پخش خودکار ویدیوها پس از پایان'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': 'वीडियो समाप्त होने पर स्वचालित रूप से दोबारा चलाएं'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': 'ビデオ終了時に自動的にリプレイする'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': '비디오가 끝나면 자동으로 다시 재생'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': 'Автоматически воспроизводить видео заново после окончания'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': 'เล่นวิดีโอซ้ำอัตโนมัติเมื่อจบ'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': '影片播放結束後自動重新播放'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': '视频播放结束后自动重新播放'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue