feat: add "Are you still watching" modal overlay with configurable options (#663)

Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
Ahmed Sbai
2025-05-18 09:21:50 +02:00
committed by GitHub
parent 963a54a36c
commit 99938ddf5a
22 changed files with 784 additions and 544 deletions

View File

@@ -66,11 +66,14 @@ export const PlayButton: React.FC<Props> = ({
const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings();
const [settings, updateSettings] = useSettings();
const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback(
(q: string) => {
if (settings.maxAutoPlayEpisodeCount.value !== -1) {
updateSettings({ autoPlayEpisodeCount: 0 });
}
router.push(`/player/direct-player?${q}`);
},
[router],

View File

@@ -10,6 +10,7 @@ import {
} from "@/utils/background-tasks";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import i18n, { TFunction } from "i18next";
import type React from "react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -251,7 +252,46 @@ export const OtherSettings: React.FC = () => {
}
/>
</ListItem>
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
<Dropdown
data={AUTOPLAY_EPISODES_COUNT(t)}
keyExtractor={(item) => item.key}
titleExtractor={(item) => item.key}
title={
<TouchableOpacity className='flex flex-row items-center justify-between py-3 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(settings?.maxAutoPlayEpisodeCount.key)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</TouchableOpacity>
}
label={t("home.settings.other.max_auto_play_episode_count")}
onSelected={(maxAutoPlayEpisodeCount) =>
updateSettings({ maxAutoPlayEpisodeCount })
}
/>
</ListItem>
</ListGroup>
</DisabledSetting>
);
};
const AUTOPLAY_EPISODES_COUNT = (
t: TFunction<"translation", undefined>,
): {
key: string;
value: number;
}[] => [
{ key: t("home.settings.other.disabled"), value: -1 },
{ key: "1", value: 1 },
{ key: "2", value: 2 },
{ key: "3", value: 3 },
{ key: "4", value: 4 },
{ key: "5", value: 5 },
{ key: "6", value: 6 },
{ key: "7", value: 7 },
];

View File

@@ -0,0 +1,49 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { useSettings } from "@/utils/atoms/settings";
import { useRouter } from "expo-router";
import { t } from "i18next";
import React from "react";
import { View } from "react-native";
export interface ContinueWatchingOverlayProps {
goToNextItem: (options: {
isAutoPlay: boolean;
resetWatchCount: boolean;
}) => void;
}
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
goToNextItem,
}) => {
const [settings] = useSettings();
const router = useRouter();
return settings.autoPlayEpisodeCount >=
settings.maxAutoPlayEpisodeCount.value ? (
<View
className={
"absolute top-0 bottom-0 left-0 right-0 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
}
>
<Text className='text-2xl font-bold text-white py-4 '>
Are you still watching ?
</Text>
<Button
onPress={() => {
goToNextItem({ isAutoPlay: false, resetWatchCount: true });
}}
color={"purple"}
className='my-4 w-2/3'
>
{t("player.continue_watching")}
</Button>
<Button onPress={router.back} color={"transparent"} className='w-2/3'>
{t("player.go_back")}
</Button>
</View>
) : null;
};
export default ContinueWatchingOverlay;

View File

@@ -1,5 +1,6 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
import { useHaptic } from "@/hooks/useHaptic";
@@ -28,7 +29,7 @@ import { Image } from "expo-image";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useAtom } from "jotai";
import { debounce } from "lodash";
import {
import React, {
type Dispatch,
type FC,
type MutableRefObject,
@@ -121,7 +122,7 @@ export const Controls: FC<Props> = ({
enableTrickplay = true,
isVlc = false,
}) => {
const [settings] = useSettings();
const [settings, updateSettings] = useSettings();
const router = useRouter();
const insets = useSafeAreaInsets();
const [api] = useAtom(apiAtom);
@@ -236,15 +237,76 @@ export const Controls: FC<Props> = ({
goToItemCommon(previousItem);
}, [previousItem, goToItemCommon]);
const goToNextItem = useCallback(() => {
if (!nextItem) return;
const goToNextItem = useCallback(
({
isAutoPlay,
resetWatchCount,
}: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
if (!nextItem) {
return;
}
if (!isAutoPlay) {
// if we are not autoplaying, we won't update anything, we just go to the next item
goToItemCommon(nextItem);
}, [nextItem, goToItemCommon]);
if (resetWatchCount) {
updateSettings({
autoPlayEpisodeCount: 0,
});
}
return;
}
// Skip autoplay logic if maxAutoPlayEpisodeCount is -1
if (settings.maxAutoPlayEpisodeCount.value === -1) {
goToItemCommon(nextItem);
return;
}
if (
settings.autoPlayEpisodeCount + 1 <
settings.maxAutoPlayEpisodeCount.value
) {
goToItemCommon(nextItem);
}
// Check if the autoPlayEpisodeCount is less than maxAutoPlayEpisodeCount for the autoPlay
if (
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value
) {
// update the autoPlayEpisodeCount in settings
updateSettings({
autoPlayEpisodeCount: settings.autoPlayEpisodeCount + 1,
});
}
},
[nextItem, goToItemCommon],
);
// Add a memoized handler for autoplay next episode
const handleNextEpisodeAutoPlay = useCallback(() => {
goToNextItem({ isAutoPlay: true });
}, [goToNextItem]);
// Add a memoized handler for manual next episode
const handleNextEpisodeManual = useCallback(() => {
goToNextItem({ isAutoPlay: false });
}, [goToNextItem]);
// Add a memoized handler for ContinueWatchingOverlay
const handleContinueWatching = useCallback(
(options: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
goToNextItem(options);
},
[goToNextItem],
);
const goToItem = useCallback(
async (itemId: string) => {
const gotoItem = await getItemById(api, itemId);
if (!gotoItem) return;
if (!gotoItem) {
return;
}
goToItemCommon(gotoItem);
},
[goToItemCommon, api],
@@ -300,7 +362,9 @@ export const Controls: FC<Props> = ({
};
const handleSliderStart = useCallback(() => {
if (!showControls) return;
if (!showControls) {
return;
}
setIsSliding(true);
wasPlayingRef.current = isPlaying;
@@ -339,7 +403,9 @@ export const Controls: FC<Props> = ({
);
const handleSkipBackward = useCallback(async () => {
if (!settings?.rewindSkipTime) return;
if (!settings?.rewindSkipTime) {
return;
}
wasPlayingRef.current = isPlaying;
lightHapticFeedback();
try {
@@ -371,7 +437,9 @@ export const Controls: FC<Props> = ({
? curr + secondsToMs(settings.forwardSkipTime)
: ticksToSeconds(curr) + settings.forwardSkipTime;
seek(Math.max(0, newTime));
if (wasPlayingRef.current) play();
if (wasPlayingRef.current) {
play();
}
}
} catch (error) {
writeToLog("ERROR", "Error seeking video forwards", error);
@@ -546,7 +614,7 @@ export const Controls: FC<Props> = ({
{nextItem && !offline && (
<TouchableOpacity
onPress={goToNextItem}
onPress={() => goToNextItem({ isAutoPlay: false })}
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
>
<Ionicons name='play-skip-forward' size={24} color='white' />
@@ -741,6 +809,9 @@ export const Controls: FC<Props> = ({
onPress={skipCredit}
buttonText='Skip Credits'
/>
{(settings.maxAutoPlayEpisodeCount.value === -1 ||
settings.autoPlayEpisodeCount <
settings.maxAutoPlayEpisodeCount.value) && (
<NextEpisodeCountDownButton
show={
!nextItem
@@ -749,9 +820,10 @@ export const Controls: FC<Props> = ({
? remainingTime < 10000
: remainingTime < 10
}
onFinish={goToNextItem}
onPress={goToNextItem}
onFinish={handleNextEpisodeAutoPlay}
onPress={handleNextEpisodeManual}
/>
)}
</View>
</View>
<View
@@ -799,6 +871,9 @@ export const Controls: FC<Props> = ({
</View>
</>
)}
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
<ContinueWatchingOverlay goToNextItem={handleContinueWatching} />
)}
</ControlProvider>
);
};

View File

@@ -79,7 +79,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
>
<Animated.View style={animatedStyle} />
<View className='px-3 py-3'>
<Text className='text-center font-bold'>
<Text numberOfLines={1} className='text-center font-bold'>
{t("player.next_episode")}
</Text>
</View>

View File

@@ -11,8 +11,8 @@ import ja from "./translations/ja.json";
import nl from "./translations/nl.json";
import pl from "./translations/pl.json";
import ptBR from "./translations/pt-BR.json";
import sv from "./translations/sv.json";
import ru from "./translations/ru.json";
import sv from "./translations/sv.json";
import tr from "./translations/tr.json";
import uk from "./translations/uk.json";
import zhCN from "./translations/zh-CN.json";

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Bibliotheken ausblenden",
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
"disable_haptic_feedback": "Haptisches Feedback deaktivieren",
"default_quality": "Standardqualität"
"default_quality": "Standardqualität",
"disabled": "Deaktiviert"
},
"downloads": {
"downloads_title": "Downloads",
@@ -370,7 +371,9 @@
"audio_tracks": "Audiospuren:",
"playback_state": "Wiedergabestatus:",
"no_data_available": "Keine Daten verfügbar",
"index": "Index:"
"index": "Index:",
"continue_watching": "Weiterschauen",
"go_back": "Zurück"
},
"item_card": {
"next_up": "Als Nächstes",

View File

@@ -138,7 +138,9 @@
"hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback",
"default_quality": "Default quality"
"default_quality": "Default quality",
"max_auto_play_episode_count": "Max auto play episode count",
"disabled": "Disabled"
},
"downloads": {
"downloads_title": "Downloads",
@@ -374,7 +376,9 @@
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"no_data_available": "No data available",
"index": "Index:"
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go back"
},
"item_card": {
"next_up": "Next up",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
"disable_haptic_feedback": "Desactivar feedback háptico",
"default_quality": "Calidad por defecto"
"default_quality": "Calidad por defecto",
"disabled": "Deshabilitado"
},
"downloads": {
"downloads_title": "Descargas",
@@ -370,7 +371,9 @@
"audio_tracks": "Pistas de audio:",
"playback_state": "Estado de la reproducción:",
"no_data_available": "No hay datos disponibles",
"index": "Índice:"
"index": "Índice:",
"continue_watching": "Continuar viendo",
"go_back": "Volver"
},
"item_card": {
"next_up": "A continuación",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Cacher des bibliothèques",
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
"disable_haptic_feedback": "Désactiver le retour haptique",
"default_quality": "Qualité par défaut"
"default_quality": "Qualité par défaut",
"disabled": "Désactivé"
},
"downloads": {
"downloads_title": "Téléchargements",
@@ -370,7 +371,9 @@
"audio_tracks": "Pistes audio:",
"playback_state": "État de lecture:",
"no_data_available": "Aucune donnée disponible",
"index": "Index:"
"index": "Index :",
"continue_watching": "Continuer à regarder",
"go_back": "Retour"
},
"item_card": {
"next_up": "À suivre",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Nascondi Librerie",
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
"disable_haptic_feedback": "Disabilita il feedback aptico",
"default_quality": "Qualità predefinita"
"default_quality": "Qualità predefinita",
"disabled": "Disabilitato"
},
"downloads": {
"downloads_title": "Scaricamento",
@@ -370,7 +371,9 @@
"audio_tracks": "Tracce audio:",
"playback_state": "Stato della riproduzione:",
"no_data_available": "Nessun dato disponibile",
"index": "Indice:"
"index": "Indice:",
"continue_watching": "Continua a guardare",
"go_back": "Indietro"
},
"item_card": {
"next_up": "Il prossimo",

View File

@@ -152,7 +152,9 @@
"optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
"read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:ポート"
"server_url_placeholder": "http(s)://domain.org:ポート",
"default_quality": "デフォルトの品質",
"disabled": "無効"
},
"plugins": {
"plugins_title": "プラグイン",
@@ -369,7 +371,9 @@
"audio_tracks": "音声トラック:",
"playback_state": "再生状態:",
"no_data_available": "データなし",
"index": "インデックス:"
"index": "インデックス:",
"continue_watching": "視聴を続ける",
"go_back": "戻る"
},
"item_card": {
"next_up": "次",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Verberg Bibliotheken",
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
"disable_haptic_feedback": "Haptische feedback uitschakelen",
"default_quality": "Standaard kwaliteit"
"default_quality": "Standaard kwaliteit",
"disabled": "Uitgeschakeld"
},
"downloads": {
"downloads_title": "Downloads",
@@ -370,7 +371,9 @@
"audio_tracks": "Audio Tracks:",
"playback_state": "Afspeelstatus:",
"no_data_available": "Geen data beschikbaar",
"index": "Index:"
"index": "Index:",
"continue_watching": "Verder kijken",
"go_back": "Terug"
},
"item_card": {
"next_up": "Volgende",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ukryj biblioteki",
"select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
"disable_haptic_feedback": "Wyłącz wibracje",
"default_quality": "Domyślna jakość"
"default_quality": "Domyślna jakość",
"disabled": "Wyłączone"
},
"downloads": {
"downloads_title": "Pobieranie",
@@ -374,7 +375,9 @@
"audio_tracks": "Ścieżki audio:",
"playback_state": "Stan odtwarzania:",
"no_data_available": "Brak dostępnych danych",
"index": "Indeks:"
"index": "Indeks:",
"continue_watching": "Kontynuuj oglądanie",
"go_back": "Wstecz"
},
"item_card": {
"next_up": "Następne",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.",
"disable_haptic_feedback": "Desativar o feedback háptico",
"default_quality": "Qualidade padrão"
"default_quality": "Qualidade padrão",
"disabled": "Desativado"
},
"downloads": {
"downloads_title": "Downloads",
@@ -371,7 +372,9 @@
"audio_tracks": "Faixas do áudio:",
"playback_state": "Playback State:",
"no_data_available": "Nenhum dado disponível",
"index": "Índice:"
"index": "Índice:",
"continue_watching": "Continuar assistindo",
"go_back": "Voltar"
},
"item_card": {
"next_up": "Próximo em",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Скрыть библиотеки",
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
"disable_haptic_feedback": "Отключить тактильную обратную связь",
"default_quality": "Качество по умолчанию"
"default_quality": "Качество по умолчанию",
"disabled": "Отключено"
},
"downloads": {
"downloads_title": "Загрузки",
@@ -198,7 +199,7 @@
"app_usage": "Приложение {{usedSpace}}%",
"device_usage": "Устройство {{availableSpace}}%",
"size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы",
"delete_all_downloaded_files": "Удалить все загруженные файлы"
},
"intro": {
"show_intro": "Показать вступление",
@@ -207,7 +208,7 @@
"logs": {
"logs_title": "Логи",
"no_logs_available": "Логи не доступны",
"delete_all_logs": "Удалить все логи",
"delete_all_logs": "Удалить все логи"
},
"languages": {
"title": "Языки",
@@ -226,7 +227,7 @@
},
"sessions": {
"title": "Сессии",
"no_active_sessions": "Нет активных сессий",
"no_active_sessions": "Нет активных сессий"
},
"downloads": {
"downloads_title": "Загрузки",
@@ -311,7 +312,7 @@
"tmdb_studio": "TMDB Студии",
"tmdb_network": "TMDB Сеть",
"tmdb_movie_streaming_services": "TMDB Потоковые сервисы фильмов",
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов",
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов"
},
"library": {
"no_items_found": "элементы не найдены",
@@ -331,7 +332,7 @@
"poster": "Постер",
"cover": "Обложка",
"show_titles": "Показывать загаловки",
"show_stats": "Показывать статистику",
"show_stats": "Показывать статистику"
},
"filters": {
"genres": "Жанры",
@@ -371,7 +372,9 @@
"audio_tracks": "Аудио дорожки:",
"playback_state": "Состояние воспроизведения:",
"no_data_available": "Данные не доступны",
"index": "Индекс:"
"index": "Индекс:",
"continue_watching": "Продолжить просмотр",
"go_back": "Назад"
},
"item_card": {
"next_up": "Следующее",
@@ -403,7 +406,7 @@
"download_x_item": "Загрузить {{item_count}} элементов",
"download_button": "Загрузить",
"using_optimized_server": "Использовать оптимизированный сервер",
"using_default_method": "Использовать стандартный метод",
"using_default_method": "Использовать стандартный метод"
}
},
"live_tv": {
@@ -474,5 +477,4 @@
"custom_links": "Кастомные ссылки",
"favorites": "Избранное"
}
}
}

View File

@@ -30,5 +30,24 @@
"home": "Hem",
"search": "Sök",
"library": "Bibliotek"
},
"player": {
"error": "Fel",
"failed_to_get_stream_url": "Kunde inte hämta stream-URL",
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
"client_error": "Klientfel",
"could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast",
"message_from_server": "Meddelande från servern: {{message}}",
"video_has_finished_playing": "Videon har spelat klart!",
"no_video_source": "Ingen videokälla...",
"next_episode": "Nästa avsnitt",
"refresh_tracks": "Uppdatera spår",
"subtitle_tracks": "Textspår:",
"audio_tracks": "Ljudspår:",
"playback_state": "Uppspelningsstatus:",
"no_data_available": "Inga data tillgängliga",
"index": "Index:",
"continue_watching": "Fortsätt titta",
"go_back": "Tillbaka"
}
}

View File

@@ -137,7 +137,9 @@
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
"hide_libraries": "Kütüphaneleri Gizle",
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak"
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak",
"default_quality": "Varsayılan kalite",
"disabled": "Devre dışı"
},
"downloads": {
"downloads_title": "İndirmeler",
@@ -369,7 +371,9 @@
"audio_tracks": "Ses Parçaları:",
"playback_state": "Oynatma Durumu:",
"no_data_available": "Veri bulunamadı",
"index": "İndeks:"
"index": "İndeks:",
"continue_watching": "İzlemeye devam et",
"go_back": "Geri"
},
"item_card": {
"next_up": "Sıradaki",

View File

@@ -137,8 +137,9 @@
"show_custom_menu_links": "Показати користувацькі посилання меню",
"hide_libraries": "Сховати медіатеки",
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
"disable_haptic_feedback": "Вимкнути тактильний відгук",
"default_quality": "Якість за замовченням"
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
"default_quality": "Якість за замовченням",
"disabled": "Вимкнено"
},
"downloads": {
"downloads_title": "Завантаження",
@@ -374,7 +375,9 @@
"audio_tracks": "Аудіо-доріжки:",
"playback_state": "Стан відтворення:",
"no_data_available": "Дані відсутні",
"index": "Індекс:"
"index": "Індекс:",
"continue_watching": "Продовжити перегляд",
"go_back": "Назад"
},
"item_card": {
"next_up": "Далі",

View File

@@ -369,7 +369,9 @@
"audio_tracks": "音频轨道:",
"playback_state": "播放状态:",
"no_data_available": "无可用数据",
"index": "索引:"
"index": "索引:",
"continue_watching": "继续观看",
"go_back": "返回"
},
"item_card": {
"next_up": "下一个",

View File

@@ -137,7 +137,9 @@
"show_custom_menu_links": "顯示自定義菜單鏈接",
"hide_libraries": "隱藏媒體庫",
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
"disable_haptic_feedback": "禁用觸覺回饋"
"disable_haptic_feedback": "禁用觸覺回饋",
"default_quality": "預設品質",
"disabled": "已停用"
},
"downloads": {
"downloads_title": "下載",
@@ -369,7 +371,9 @@
"audio_tracks": "音頻軌道:",
"playback_state": "播放狀態:",
"no_data_available": "無可用數據",
"index": "索引:"
"index": "索引:",
"continue_watching": "繼續觀看",
"go_back": "返回"
},
"item_card": {
"next_up": "下一個",

View File

@@ -114,6 +114,11 @@ export type HomeSectionNextUpResolver = {
enableRewatching?: boolean;
};
export interface MaxAutoPlayEpisodeCount {
key: string;
value: number;
}
export type HomeSectionLatestResolver = {
parentId?: string;
limit?: number;
@@ -163,6 +168,8 @@ export type Settings = {
hiddenLibraries?: string[];
enableH265ForChromecast: boolean;
defaultPlayer: VideoPlayer;
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
autoPlayEpisodeCount: number;
};
export interface Lockable<T> {
@@ -217,7 +224,9 @@ const defaultValues: Settings = {
jellyseerrServerUrl: undefined,
hiddenLibraries: [],
enableH265ForChromecast: false,
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
defaultPlayer: VideoPlayer.VLC_3, // ios-only setting. does not matter what this is for android
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
autoPlayEpisodeCount: 0,
};
const loadSettings = (): Partial<Settings> => {
@@ -236,11 +245,11 @@ const loadSettings = (): Partial<Settings> => {
const EXCLUDE_FROM_SAVE = ["home"];
const saveSettings = (settings: Settings) => {
Object.keys(settings).forEach((key) => {
for (const key of Object.keys(settings)) {
if (EXCLUDE_FROM_SAVE.includes(key)) {
delete settings[key as keyof Settings];
}
});
}
const jsonValue = JSON.stringify(settings);
storage.set("settings", jsonValue);
};
@@ -271,7 +280,9 @@ export const useSettings = () => {
);
const refreshStreamyfinPluginSettings = useCallback(async () => {
if (!api) return;
if (!api) {
return;
}
const settings = await api.getStreamyfinPluginConfig().then(
({ data }) => {
writeInfoLog("Got plugin settings", data?.settings);
@@ -284,7 +295,9 @@ export const useSettings = () => {
}, [api]);
const updateSettings = (update: Partial<Settings>) => {
if (!_settings) return;
if (!_settings) {
return;
}
const hasChanges = Object.entries(update).some(
([key, value]) => _settings[key as keyof Settings] !== value,
);
@@ -305,34 +318,31 @@ export const useSettings = () => {
// If admin sets locked to false but provides a value,
// use user settings first and fallback on admin setting if required.
const settings: Settings = useMemo(() => {
let unlockedPluginDefaults = {} as Settings;
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
(acc, [key, setting]) => {
const unlockedPluginDefaults = {} as Settings;
const overrideSettings = Object.entries(pluginSettings ?? {}).reduce<
Partial<Settings>
>((acc, [key, setting]) => {
if (setting) {
const { value, locked } = setting;
const settingsKey = key as keyof Settings;
// Make sure we override default settings with plugin settings when they are not locked.
// Admin decided what users defaults should be and grants them the ability to change them too.
if (
locked === false &&
value &&
_settings?.[key as keyof Settings] !== value
!locked &&
value !== undefined &&
_settings?.[settingsKey] !== value
) {
unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, {
[key as keyof Settings]: value,
Object.assign(unlockedPluginDefaults, {
[settingsKey]: value,
});
}
acc = Object.assign(acc, {
[key]: locked
? value
: (_settings?.[key as keyof Settings] ?? value),
Object.assign(acc, {
[settingsKey]: locked ? value : (_settings?.[settingsKey] ?? value),
});
}
return acc;
},
{} as Settings,
);
}, {});
return {
...defaultValues,