Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
8a6b9fd055 chore(deps): Update dependency react-native-worklets to v0.10.2 2026-07-09 19:04:43 +00:00
46 changed files with 1375 additions and 775 deletions

View File

@@ -20,12 +20,7 @@ concurrency:
jobs: jobs:
comment: comment:
# Only run when the lint run actually produced a result artifact: a run if: github.event.workflow_run.event == 'pull_request'
# cancelled by concurrency (rapid pushes to the same PR) completes with
# conclusion "cancelled" before uploading it, and would fail the download.
if: >
github.event.workflow_run.event == 'pull_request' &&
contains(fromJSON('["success", "failure"]'), github.event.workflow_run.conclusion)
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: ⬇️ Download lint result - name: ⬇️ Download lint result

View File

@@ -1,8 +1,11 @@
import { ScrollView, View } from "react-native"; import { ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { useSettings } from "@/utils/atoms/settings";
export default function JellyseerrPluginPage() { export default function JellyseerrPluginPage() {
const { pluginSettings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (
@@ -13,9 +16,12 @@ export default function JellyseerrPluginPage() {
paddingRight: insets.right, paddingRight: insets.right,
}} }}
> >
<View className='p-4'> <DisabledSetting
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true}
className='p-4'
>
<JellyseerrSettings /> <JellyseerrSettings />
</View> </DisabledSetting>
</ScrollView> </ScrollView>
); );
} }

View File

@@ -1,8 +1,11 @@
import { ScrollView, View } from "react-native"; import { ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { KefinTweaksSettings } from "@/components/settings/KefinTweaks"; import { KefinTweaksSettings } from "@/components/settings/KefinTweaks";
import { useSettings } from "@/utils/atoms/settings";
export default function KefinTweaksPage() { export default function KefinTweaksPage() {
const { pluginSettings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (
@@ -13,9 +16,12 @@ export default function KefinTweaksPage() {
paddingRight: insets.right, paddingRight: insets.right,
}} }}
> >
<View className='px-4'> <DisabledSetting
disabled={pluginSettings?.useKefinTweaks?.locked === true}
className='p-4'
>
<KefinTweaksSettings /> <KefinTweaksSettings />
</View> </DisabledSetting>
</ScrollView> </ScrollView>
); );
} }

View File

@@ -1,5 +1,5 @@
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Linking, Linking,
@@ -14,22 +14,22 @@ import { toast } from "sonner-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup"; import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem"; import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient"; import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
export default function MarlinSearchPage() { export default function MarlinSearchPage() {
const navigation = useNavigation(); const navigation = useNavigation();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { settings, updateSettings, pluginSettings } = useSettings(); const { settings, updateSettings, pluginSettings } = useSettings();
const queryClient = useNetworkAwareQueryClient(); const queryClient = useNetworkAwareQueryClient();
const [value, setValue] = useState<string>(settings?.marlinServerUrl || ""); const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
const searchEngineLocked = pluginSettings?.searchEngine?.locked === true;
const marlinUrlLocked = pluginSettings?.marlinServerUrl?.locked === true;
const hasStreamystats = !!pluginSettings?.streamyStatsServerUrl?.value;
const onSave = (val: string) => { const onSave = (val: string) => {
updateSettings({ updateSettings({
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1), marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
@@ -41,8 +41,15 @@ export default function MarlinSearchPage() {
Linking.openURL("https://github.com/fredrikburmester/marlin-search"); Linking.openURL("https://github.com/fredrikburmester/marlin-search");
}; };
const disabled = useMemo(() => {
return (
pluginSettings?.searchEngine?.locked === true &&
pluginSettings?.marlinServerUrl?.locked === true
);
}, [pluginSettings]);
useEffect(() => { useEffect(() => {
if (!marlinUrlLocked) { if (!pluginSettings?.marlinServerUrl?.locked) {
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<TouchableOpacity onPress={() => onSave(value)} className='px-2'> <TouchableOpacity onPress={() => onSave(value)} className='px-2'>
@@ -53,7 +60,7 @@ export default function MarlinSearchPage() {
), ),
}); });
} }
}, [navigation, value, marlinUrlLocked, t]); }, [navigation, value, pluginSettings?.marlinServerUrl?.locked, t]);
if (!settings) return null; if (!settings) return null;
@@ -65,15 +72,19 @@ export default function MarlinSearchPage() {
paddingRight: insets.right, paddingRight: insets.right,
}} }}
> >
<View className='px-4'> <DisabledSetting disabled={disabled} className='px-4'>
<ListGroup> <ListGroup>
{/* disabledByAdmin renders the "Disabled by admin" notice as the row's <DisabledSetting
subtitle (same pattern as the Streamystats settings) — no clipping. */} disabled={
pluginSettings?.searchEngine?.locked === true ||
!!pluginSettings?.streamyStatsServerUrl?.value
}
showText={!pluginSettings?.marlinServerUrl?.locked}
>
<ListItem <ListItem
title={t( title={t(
"home.settings.plugins.marlin_search.enable_marlin_search", "home.settings.plugins.marlin_search.enable_marlin_search",
)} )}
disabledByAdmin={searchEngineLocked}
onPress={() => { onPress={() => {
updateSettings({ searchEngine: "Jellyfin" }); updateSettings({ searchEngine: "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] }); queryClient.invalidateQueries({ queryKey: ["search"] });
@@ -81,23 +92,32 @@ export default function MarlinSearchPage() {
> >
<Switch <Switch
value={settings.searchEngine === "Marlin"} value={settings.searchEngine === "Marlin"}
disabled={searchEngineLocked || hasStreamystats} disabled={!!pluginSettings?.streamyStatsServerUrl?.value}
onValueChange={(val) => { onValueChange={(value) => {
updateSettings({ searchEngine: val ? "Marlin" : "Jellyfin" }); updateSettings({
searchEngine: value ? "Marlin" : "Jellyfin",
});
queryClient.invalidateQueries({ queryKey: ["search"] }); queryClient.invalidateQueries({ queryKey: ["search"] });
}} }}
/> />
</ListItem> </ListItem>
</DisabledSetting>
</ListGroup> </ListGroup>
<ListGroup className='mt-2'> <DisabledSetting
<ListItem disabled={pluginSettings?.marlinServerUrl?.locked === true}
title={t("home.settings.plugins.marlin_search.url")} showText={!pluginSettings?.searchEngine?.locked}
disabledByAdmin={marlinUrlLocked} className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
> >
<View
className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}
>
<Text className='mr-4'>
{t("home.settings.plugins.marlin_search.url")}
</Text>
<TextInput <TextInput
editable={!marlinUrlLocked && settings.searchEngine === "Marlin"} editable={settings.searchEngine === "Marlin"}
className='text-white text-right flex-1' className='text-white'
placeholder={t( placeholder={t(
"home.settings.plugins.marlin_search.server_url_placeholder", "home.settings.plugins.marlin_search.server_url_placeholder",
)} )}
@@ -108,16 +128,15 @@ export default function MarlinSearchPage() {
textContentType='URL' textContentType='URL'
onChangeText={(text) => setValue(text)} onChangeText={(text) => setValue(text)}
/> />
</ListItem> </View>
</ListGroup> </DisabledSetting>
<Text className='px-4 text-xs text-neutral-500 mt-1'> <Text className='px-4 text-xs text-neutral-500 mt-1'>
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "} {t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
<Text className='text-blue-500' onPress={handleOpenLink}> <Text className='text-blue-500' onPress={handleOpenLink}>
{t("home.settings.plugins.marlin_search.read_more_about_marlin")} {t("home.settings.plugins.marlin_search.read_more_about_marlin")}
</Text> </Text>
</Text> </Text>
</View> </DisabledSetting>
</ScrollView> </ScrollView>
); );
} }

View File

@@ -1,21 +1,9 @@
import { useCallback } from "react"; import { Platform, ScrollView, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { PluginSettings } from "@/components/settings/PluginSettings"; import { PluginSettings } from "@/components/settings/PluginSettings";
import { useSettings } from "@/utils/atoms/settings";
export default function PluginsPage() { export default function PluginsPage() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation();
const { refreshStreamyfinPluginSettings } = useSettings();
const handleRefreshFromServer = useCallback(async () => {
await refreshStreamyfinPluginSettings();
toast.success(t("home.settings.plugins.streamystats.toasts.refreshed"));
}, [refreshStreamyfinPluginSettings, t]);
return ( return (
<ScrollView <ScrollView
@@ -30,17 +18,6 @@ export default function PluginsPage() {
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }} style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
> >
<PluginSettings /> <PluginSettings />
{/* Pulls the centralised Streamyfin plugin settings for every plugin,
so it lives on the plugins index rather than inside Streamystats. */}
<TouchableOpacity
onPress={handleRefreshFromServer}
className='py-3 rounded-xl bg-neutral-800'
>
<Text className='text-center text-blue-500'>
{t("home.settings.plugins.streamystats.refresh_from_server")}
</Text>
</TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
); );

View File

@@ -22,7 +22,12 @@ export default function StreamystatsPage() {
const navigation = useNavigation(); const navigation = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { settings, updateSettings, pluginSettings } = useSettings(); const {
settings,
updateSettings,
pluginSettings,
refreshStreamyfinPluginSettings,
} = useSettings();
const queryClient = useNetworkAwareQueryClient(); const queryClient = useNetworkAwareQueryClient();
// Local state for all editable fields // Local state for all editable fields
@@ -44,21 +49,7 @@ export default function StreamystatsPage() {
); );
const isUrlLocked = pluginSettings?.streamyStatsServerUrl?.locked === true; const isUrlLocked = pluginSettings?.streamyStatsServerUrl?.locked === true;
const searchLocked = pluginSettings?.searchEngine?.locked === true; const isStreamystatsEnabled = !!url;
const movieRecsLocked =
pluginSettings?.streamyStatsMovieRecommendations?.locked === true;
const seriesRecsLocked =
pluginSettings?.streamyStatsSeriesRecommendations?.locked === true;
const promotedWatchlistsLocked =
pluginSettings?.streamyStatsPromotedWatchlists?.locked === true;
const hideWatchlistsTabLocked =
pluginSettings?.hideWatchlistsTab?.locked === true;
// The input renders the locked admin URL; enablement must follow the same
// effective value or every toggle stays disabled until local state syncs.
const effectiveUrl = isUrlLocked
? (settings?.streamyStatsServerUrl ?? "")
: url;
const isStreamystatsEnabled = !!effectiveUrl;
const onSave = useCallback(() => { const onSave = useCallback(() => {
const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url; const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url;
@@ -122,6 +113,17 @@ export default function StreamystatsPage() {
Linking.openURL("https://github.com/fredrikburmester/streamystats"); Linking.openURL("https://github.com/fredrikburmester/streamystats");
}; };
const handleRefreshFromServer = useCallback(async () => {
const newPluginSettings = await refreshStreamyfinPluginSettings();
// Update local state with new values
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
setUrl(newUrl);
if (newUrl) {
setUseForSearch(true);
}
toast.success(t("home.settings.plugins.streamystats.toasts.refreshed"));
}, [refreshStreamyfinPluginSettings, t]);
if (!settings) return null; if (!settings) return null;
return ( return (
@@ -144,7 +146,7 @@ export default function StreamystatsPage() {
placeholder={t( placeholder={t(
"home.settings.plugins.streamystats.server_url_placeholder", "home.settings.plugins.streamystats.server_url_placeholder",
)} )}
value={effectiveUrl} value={url}
keyboardType='url' keyboardType='url'
returnKeyType='done' returnKeyType='done'
autoCapitalize='none' autoCapitalize='none'
@@ -169,18 +171,11 @@ export default function StreamystatsPage() {
> >
<ListItem <ListItem
title={t("home.settings.plugins.streamystats.enable_search")} title={t("home.settings.plugins.streamystats.enable_search")}
disabledByAdmin={searchLocked} disabledByAdmin={pluginSettings?.searchEngine?.locked === true}
> >
{/* Locked controls show the live admin value and can't be toggled —
local form state would let the switch flip while the write guard
drops the change. */}
<Switch <Switch
value={ value={useForSearch}
searchLocked disabled={!isStreamystatsEnabled}
? settings?.searchEngine === "Streamystats"
: useForSearch
}
disabled={!isStreamystatsEnabled || searchLocked}
onValueChange={setUseForSearch} onValueChange={setUseForSearch}
/> />
</ListItem> </ListItem>
@@ -188,62 +183,52 @@ export default function StreamystatsPage() {
title={t( title={t(
"home.settings.plugins.streamystats.enable_movie_recommendations", "home.settings.plugins.streamystats.enable_movie_recommendations",
)} )}
disabledByAdmin={movieRecsLocked} disabledByAdmin={
pluginSettings?.streamyStatsMovieRecommendations?.locked === true
}
> >
<Switch <Switch
value={ value={movieRecs}
movieRecsLocked
? (settings?.streamyStatsMovieRecommendations ?? false)
: movieRecs
}
onValueChange={setMovieRecs} onValueChange={setMovieRecs}
disabled={!isStreamystatsEnabled || movieRecsLocked} disabled={!isStreamystatsEnabled}
/> />
</ListItem> </ListItem>
<ListItem <ListItem
title={t( title={t(
"home.settings.plugins.streamystats.enable_series_recommendations", "home.settings.plugins.streamystats.enable_series_recommendations",
)} )}
disabledByAdmin={seriesRecsLocked} disabledByAdmin={
pluginSettings?.streamyStatsSeriesRecommendations?.locked === true
}
> >
<Switch <Switch
value={ value={seriesRecs}
seriesRecsLocked
? (settings?.streamyStatsSeriesRecommendations ?? false)
: seriesRecs
}
onValueChange={setSeriesRecs} onValueChange={setSeriesRecs}
disabled={!isStreamystatsEnabled || seriesRecsLocked} disabled={!isStreamystatsEnabled}
/> />
</ListItem> </ListItem>
<ListItem <ListItem
title={t( title={t(
"home.settings.plugins.streamystats.enable_promoted_watchlists", "home.settings.plugins.streamystats.enable_promoted_watchlists",
)} )}
disabledByAdmin={promotedWatchlistsLocked} disabledByAdmin={
pluginSettings?.streamyStatsPromotedWatchlists?.locked === true
}
> >
<Switch <Switch
value={ value={promotedWatchlists}
promotedWatchlistsLocked
? (settings?.streamyStatsPromotedWatchlists ?? false)
: promotedWatchlists
}
onValueChange={setPromotedWatchlists} onValueChange={setPromotedWatchlists}
disabled={!isStreamystatsEnabled || promotedWatchlistsLocked} disabled={!isStreamystatsEnabled}
/> />
</ListItem> </ListItem>
<ListItem <ListItem
title={t("home.settings.plugins.streamystats.hide_watchlists_tab")} title={t("home.settings.plugins.streamystats.hide_watchlists_tab")}
disabledByAdmin={hideWatchlistsTabLocked} disabledByAdmin={pluginSettings?.hideWatchlistsTab?.locked === true}
> >
<Switch <Switch
value={ value={hideWatchlistsTab}
hideWatchlistsTabLocked
? (settings?.hideWatchlistsTab ?? false)
: hideWatchlistsTab
}
onValueChange={setHideWatchlistsTab} onValueChange={setHideWatchlistsTab}
disabled={!isStreamystatsEnabled || hideWatchlistsTabLocked} disabled={!isStreamystatsEnabled}
/> />
</ListItem> </ListItem>
</ListGroup> </ListGroup>
@@ -251,6 +236,15 @@ export default function StreamystatsPage() {
{t("home.settings.plugins.streamystats.home_sections_hint")} {t("home.settings.plugins.streamystats.home_sections_hint")}
</Text> </Text>
<TouchableOpacity
onPress={handleRefreshFromServer}
className='mt-6 py-3 rounded-xl bg-neutral-800'
>
<Text className='text-center text-blue-500'>
{t("home.settings.plugins.streamystats.refresh_from_server")}
</Text>
</TouchableOpacity>
{/* Disable button - only show if URL is not locked and Streamystats is enabled */} {/* Disable button - only show if URL is not locked and Streamystats is enabled */}
{!isUrlLocked && isStreamystatsEnabled && ( {!isUrlLocked && isStreamystatsEnabled && (
<TouchableOpacity <TouchableOpacity

View File

@@ -92,7 +92,7 @@
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.8.3", "react-native-worklets": "0.10.2",
"sonner-native": "0.21.2", "sonner-native": "0.21.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
@@ -1653,7 +1653,7 @@
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
"react-native-worklets": ["react-native-worklets@0.8.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.3" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.81 - 0.85" } }, "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg=="], "react-native-worklets": ["react-native-worklets@0.10.2", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-LX27ejYI8veeDp59Z3rjo2pYyPa9euzSH8GUlem7cnNqfsDtGum8PQpkbzrqhLsWH0CjdeHR7p3sncCyYbwaVw=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],

99
components/TrackSheet.tsx Normal file
View File

@@ -0,0 +1,99 @@
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "./common/Text";
import { FilterSheet } from "./filters/FilterSheet";
interface Props extends React.ComponentProps<typeof View> {
source?: MediaSourceInfo;
onChange: (value: number) => void;
selected?: number | undefined;
streamType?: string;
title: string;
}
export const TrackSheet: React.FC<Props> = ({
source,
onChange,
selected,
streamType,
title,
...props
}) => {
const isTv = Platform.isTV;
const { t } = useTranslation();
const streams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === streamType),
[source, streamType],
);
const selectedSteam = useMemo(
() => streams?.find((x) => x.Index === selected),
[streams, selected],
);
const noneOption = useMemo(
() => ({ Index: -1, DisplayTitle: t("common.none") }),
[t],
);
// Creates a modified data array that includes a "None" option for subtitles
// We might want to possibly do this for other places, like audio?
const addNoneToSubtitles = useMemo(() => {
if (streamType === "Subtitle") {
const result = streams ? [noneOption, ...streams] : [noneOption];
return result;
}
return streams;
}, [streams, streamType, noneOption]);
const [open, setOpen] = useState(false);
if (isTv || (streams && streams.length === 0)) return null;
return (
<View className='flex shrink' style={{ minWidth: 60 }} {...props}>
<View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'>{title}</Text>
<TouchableOpacity
className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between'
onPress={() => setOpen(true)}
>
<Text numberOfLines={1}>
{selected === -1 && streamType === "Subtitle"
? t("common.none")
: selectedSteam?.DisplayTitle || t("common.select")}
</Text>
</TouchableOpacity>
</View>
<FilterSheet
open={open}
setOpen={setOpen}
title={title}
data={addNoneToSubtitles || []}
values={
selected === -1 && streamType === "Subtitle"
? [{ Index: -1, DisplayTitle: t("common.none") }]
: selectedSteam
? [selectedSteam]
: []
}
multiple={false}
searchFilter={(item, query) => {
const label = (item as any).DisplayTitle || "";
return label.toLowerCase().includes(query.toLowerCase());
}}
renderItemLabel={(item) => (
<Text>{(item as any).DisplayTitle || ""}</Text>
)}
set={(vals) => {
const chosen = vals[0] as any;
if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
onChange(chosen.Index);
}
}}
/>
</View>
);
};

View File

@@ -1,119 +1,43 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React from "react"; import type React from "react";
import { Platform, View, type ViewStyle } from "react-native"; import { Platform, View } from "react-native";
import { Text } from "@/components/common/Text";
import { scaleSize } from "@/utils/scaleSize";
const isAggregateType = (item: BaseItemDto) =>
item.Type === "Series" || item.Type === "BoxSet";
// TV sizes are scaled relative to a 1920×1080 reference (see scaleSize).
const tvBadgeBase: ViewStyle = {
position: "absolute",
top: scaleSize(8),
right: scaleSize(8),
height: scaleSize(28),
borderRadius: scaleSize(14),
backgroundColor: "rgba(255,255,255,0.92)",
alignItems: "center",
justifyContent: "center",
};
// Mobile uses raw dp — no scaling.
const mobileBadgeBase: ViewStyle = {
position: "absolute",
top: 4,
right: 4,
height: 20,
borderRadius: 10,
backgroundColor: "#9333ea",
alignItems: "center",
justifyContent: "center",
};
/**
* Renders the unplayed-episode count badge for Series/BoxSet items that still
* have episodes left to watch. Returns null for non-aggregate types, fully
* watched items, or items with no unplayed count, so it is safe to mount
* unconditionally as an overlay (e.g. on top of the tvOS glass poster, where
* the watched checkmark is drawn natively and only the count needs RN).
*/
export const UnplayedCountBadge: React.FC<{ item: BaseItemDto }> = React.memo(
({ item }) => {
if (!isAggregateType(item)) return null;
if (item.UserData?.Played) return null;
const unplayed = item.UserData?.UnplayedItemCount ?? 0;
if (unplayed <= 0) return null;
// Cap at 1k+ to keep the badge compact (jellyfin-web caps at 99+).
const label = unplayed >= 1000 ? "1k+" : String(unplayed);
if (Platform.isTV) {
return (
<View
style={[
tvBadgeBase,
{ minWidth: scaleSize(28), paddingHorizontal: scaleSize(7) },
]}
>
<Text
style={{
fontSize: scaleSize(15),
fontWeight: "700",
color: "black",
}}
>
{label}
</Text>
</View>
);
}
return (
<View style={[mobileBadgeBase, { minWidth: 20, paddingHorizontal: 5 }]}>
<Text style={{ fontSize: 12, fontWeight: "700", color: "white" }}>
{label}
</Text>
</View>
);
},
);
export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => { export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => {
const isMovieOrEpisode = item.Type === "Movie" || item.Type === "Episode";
const isAggregate = isAggregateType(item);
const isPlayed = item.UserData?.Played === true;
if (Platform.isTV) { if (Platform.isTV) {
// Fully watched → white checkmark badge (top-right) // TV: Show white checkmark when watched
if (isPlayed && (isMovieOrEpisode || isAggregate)) { if (
item.UserData?.Played &&
(item.Type === "Movie" || item.Type === "Episode")
) {
return ( return (
<View style={[tvBadgeBase, { width: scaleSize(28) }]}> <View
<Ionicons name='checkmark' size={scaleSize(18)} color='black' /> style={{
position: "absolute",
top: 8,
right: 8,
backgroundColor: "rgba(255,255,255,0.9)",
borderRadius: 14,
width: 28,
height: 28,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name='checkmark' size={18} color='black' />
</View> </View>
); );
} }
// Series/BoxSet with remaining episodes → count badge return null;
return <UnplayedCountBadge item={item} />;
} }
// Mobile: purple corner ribbon for unwatched Movie/Episode (existing behavior) // Mobile: Show purple triangle for unwatched
return ( return (
<> <>
{/* Strict === false: items without UserData (unknown state) get no ribbon */} {item.UserData?.Played === false &&
{isMovieOrEpisode && item.UserData?.Played === false && ( (item.Type === "Movie" || item.Type === "Episode") && (
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' /> <View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' />
)} )}
{/* Fully watched Series/BoxSet → small purple checkmark */}
{isAggregate && isPlayed && (
<View style={[mobileBadgeBase, { width: 20 }]}>
<Ionicons name='checkmark' size={13} color='white' />
</View>
)}
{/* Series/BoxSet with remaining episodes → count badge */}
<UnplayedCountBadge item={item} />
</> </>
); );
}; };

View File

@@ -1,8 +1,8 @@
import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useState } from "react"; import { useState } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { ItemImage } from "../common/ItemImage"; import { ItemImage } from "../common/ItemImage";
import { WatchedIndicator } from "../WatchedIndicator";
interface Props extends ViewProps { interface Props extends ViewProps {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -3,7 +3,6 @@ import { Image } from "expo-image";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useMemo } from "react"; import { useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
@@ -53,7 +52,6 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
width: "100%", width: "100%",
}} }}
/> />
<WatchedIndicator item={item} />
</View> </View>
); );
}; };

View File

@@ -20,10 +20,7 @@ export const JellyseerrSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { settings, updateSettings, pluginSettings } = useSettings(); const { settings, updateSettings } = useSettings();
// Only the server URL is admin-lockable — the password stays editable so
// the user can still sign in to the admin-pinned Jellyseerr server.
const urlLocked = pluginSettings?.jellyseerrServerUrl?.locked === true;
const [jellyseerrPassword, setJellyseerrPassword] = useState< const [jellyseerrPassword, setJellyseerrPassword] = useState<
string | undefined string | undefined
@@ -118,7 +115,6 @@ export const JellyseerrSettings = () => {
</> </>
) : ( ) : (
<View className='flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900'> <View className='flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900'>
<View style={{ opacity: urlLocked ? 0.5 : 1 }}>
<Text className='font-bold mb-1'> <Text className='font-bold mb-1'>
{t("home.settings.plugins.jellyseerr.server_url")} {t("home.settings.plugins.jellyseerr.server_url")}
</Text> </Text>
@@ -132,11 +128,7 @@ export const JellyseerrSettings = () => {
placeholder={t( placeholder={t(
"home.settings.plugins.jellyseerr.server_url_placeholder", "home.settings.plugins.jellyseerr.server_url_placeholder",
)} )}
value={ value={jellyseerrServerUrl ?? settings?.jellyseerrServerUrl}
urlLocked
? settings?.jellyseerrServerUrl
: (jellyseerrServerUrl ?? settings?.jellyseerrServerUrl)
}
defaultValue={ defaultValue={
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
} }
@@ -145,14 +137,8 @@ export const JellyseerrSettings = () => {
autoCapitalize='none' autoCapitalize='none'
textContentType='URL' textContentType='URL'
onChangeText={setjellyseerrServerUrl} onChangeText={setjellyseerrServerUrl}
editable={!urlLocked && !loginToJellyseerrMutation.isPending} editable={!loginToJellyseerrMutation.isPending}
/> />
{urlLocked && (
<Text className='text-xs text-red-600 mb-2'>
Disabled by admin
</Text>
)}
</View>
<View> <View>
<Text className='font-bold mb-2'> <Text className='font-bold mb-2'>
{t("home.settings.plugins.jellyseerr.password")} {t("home.settings.plugins.jellyseerr.password")}

View File

@@ -1,28 +1,33 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Switch } from "react-native"; import { Switch, Text, View } from "react-native";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const KefinTweaksSettings = () => { export const KefinTweaksSettings = () => {
const { settings, updateSettings, pluginSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
const isEnabled = settings?.useKefinTweaks ?? false; const isEnabled = settings?.useKefinTweaks ?? false;
const locked = pluginSettings?.useKefinTweaks?.locked === true;
return ( return (
<ListGroup> <View className=''>
<ListItem <View className='flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900'>
title={t("home.settings.plugins.kefinTweaks.watchlist_enabler")} <Text className='text-xs text-red-600 mb-2'>
disabledByAdmin={locked} {t("home.settings.plugins.kefinTweaks.watchlist_enabler")}
> </Text>
<View className='flex flex-row items-center justify-between mt-2'>
<Text className='text-white'>
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
</Text>
<Switch <Switch
value={isEnabled} value={isEnabled}
disabled={locked}
onValueChange={(value) => updateSettings({ useKefinTweaks: value })} onValueChange={(value) => updateSettings({ useKefinTweaks: value })}
trackColor={{ false: "#555", true: "purple" }}
thumbColor={isEnabled ? "#fff" : "#ccc"}
/> />
</ListItem> </View>
</ListGroup> </View>
</View>
); );
}; };

View File

@@ -20,12 +20,7 @@ export const PluginSettings = () => {
> >
<ListItem <ListItem
onPress={() => router.push("/settings/plugins/jellyseerr/page")} onPress={() => router.push("/settings/plugins/jellyseerr/page")}
title='Jellyseerr' title={"Jellyseerr"}
showArrow
/>
<ListItem
onPress={() => router.push("/settings/plugins/streamystats/page")}
title='Streamystats'
showArrow showArrow
/> />
<ListItem <ListItem
@@ -33,6 +28,11 @@ export const PluginSettings = () => {
title='Marlin Search' title='Marlin Search'
showArrow showArrow
/> />
<ListItem
onPress={() => router.push("/settings/plugins/streamystats/page")}
title='Streamystats'
showArrow
/>
<ListItem <ListItem
onPress={() => router.push("/settings/plugins/kefinTweaks/page")} onPress={() => router.push("/settings/plugins/kefinTweaks/page")}
title='KefinTweaks' title='KefinTweaks'

View File

@@ -12,10 +12,7 @@ import {
} from "react-native"; } from "react-native";
import { ProgressBar } from "@/components/common/ProgressBar"; import { ProgressBar } from "@/components/common/ProgressBar";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { import { WatchedIndicator } from "@/components/WatchedIndicator";
UnplayedCountBadge,
WatchedIndicator,
} from "@/components/WatchedIndicator";
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes"; import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { import {
@@ -430,12 +427,6 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
/> />
{PlayButtonOverlay} {PlayButtonOverlay}
{NowPlayingBadge} {NowPlayingBadge}
{/*
The glass view draws the watched checkmark natively but cannot show
an unplayed-episode count, so render it as an RN overlay on top.
Returns null when not applicable (non-series / fully watched).
*/}
{showWatchedIndicator && <UnplayedCountBadge item={item} />}
</View> </View>
); );
} }
@@ -468,7 +459,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
/> />
{PlayButtonOverlay} {PlayButtonOverlay}
{NowPlayingBadge} {NowPlayingBadge}
{showWatchedIndicator && <WatchedIndicator item={item} />} <WatchedIndicator item={item} />
<ProgressBar item={item} /> <ProgressBar item={item} />
</View> </View>
); );

View File

@@ -0,0 +1,560 @@
import { Ionicons } from "@expo/vector-icons";
import type {
BaseItemDto,
MediaStream,
} from "@jellyfin/sdk/lib/generated-client";
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Animated,
Easing,
ScrollView,
StyleSheet,
TVFocusGuideView,
View,
} from "react-native";
import { Text } from "@/components/common/Text";
import {
TVCancelButton,
TVLanguageCard,
TVSubtitleResultCard,
TVTabButton,
TVTrackCard,
} from "@/components/tv";
import {
type SubtitleSearchResult,
useRemoteSubtitles,
} from "@/hooks/useRemoteSubtitles";
import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
interface TVSubtitleSheetProps {
visible: boolean;
item: BaseItemDto;
mediaSourceId?: string | null;
subtitleTracks: MediaStream[];
currentSubtitleIndex: number;
onSubtitleIndexChange: (index: number) => void;
onClose: () => void;
onServerSubtitleDownloaded?: () => void;
onLocalSubtitleDownloaded?: (path: string) => void;
}
type TabType = "tracks" | "download";
export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
visible,
item,
mediaSourceId,
subtitleTracks,
currentSubtitleIndex,
onSubtitleIndexChange,
onClose,
onServerSubtitleDownloaded,
onLocalSubtitleDownloaded,
}) => {
const { t } = useTranslation();
console.log(
"[TVSubtitleSheet] visible:",
visible,
"tracks:",
subtitleTracks.length,
);
const [activeTab, setActiveTab] = useState<TabType>("tracks");
const [selectedLanguage, setSelectedLanguage] = useState("eng");
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const [hasSearchedThisSession, setHasSearchedThisSession] = useState(false);
const [isReady, setIsReady] = useState(false);
const [isTabContentReady, setIsTabContentReady] = useState(false);
const firstTrackRef = useRef<View>(null);
const {
hasOpenSubtitlesApiKey,
isSearching,
searchError,
searchResults,
search,
downloadAsync,
reset,
} = useRemoteSubtitles({
itemId: item.Id ?? "",
item,
mediaSourceId,
});
const resetRef = useRef(reset);
resetRef.current = reset;
const overlayOpacity = useRef(new Animated.Value(0)).current;
const sheetTranslateY = useRef(new Animated.Value(300)).current;
const initialSelectedTrackIndex = useMemo(() => {
if (currentSubtitleIndex === -1) return 0;
const trackIdx = subtitleTracks.findIndex(
(t) => t.Index === currentSubtitleIndex,
);
return trackIdx >= 0 ? trackIdx + 1 : 0;
}, [subtitleTracks, currentSubtitleIndex]);
useEffect(() => {
if (visible) {
overlayOpacity.setValue(0);
sheetTranslateY.setValue(300);
Animated.parallel([
Animated.timing(overlayOpacity, {
toValue: 1,
duration: 250,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(sheetTranslateY, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
]).start();
}
}, [visible, overlayOpacity, sheetTranslateY]);
useEffect(() => {
if (!visible) {
setHasSearchedThisSession(false);
setActiveTab("tracks");
resetRef.current();
setIsReady(false);
}
}, [visible]);
useEffect(() => {
if (visible) {
const timer = setTimeout(() => setIsReady(true), 100);
return () => clearTimeout(timer);
}
setIsReady(false);
}, [visible]);
useEffect(() => {
if (visible && activeTab === "download" && !hasSearchedThisSession) {
search({ language: selectedLanguage });
setHasSearchedThisSession(true);
}
}, [visible, activeTab, hasSearchedThisSession, search, selectedLanguage]);
useEffect(() => {
if (isReady) {
setIsTabContentReady(false);
const timer = setTimeout(() => setIsTabContentReady(true), 50);
return () => clearTimeout(timer);
}
setIsTabContentReady(false);
}, [activeTab, isReady]);
const handleLanguageSelect = useCallback(
(code: string) => {
setSelectedLanguage(code);
search({ language: code });
},
[search],
);
const handleTrackSelect = useCallback(
(index: number) => {
onSubtitleIndexChange(index);
onClose();
},
[onSubtitleIndexChange, onClose],
);
const handleDownload = useCallback(
async (result: SubtitleSearchResult) => {
setDownloadingId(result.id);
try {
const downloadResult = await downloadAsync(result);
if (downloadResult.type === "server") {
onServerSubtitleDownloaded?.();
} else if (downloadResult.type === "local" && downloadResult.path) {
onLocalSubtitleDownloaded?.(downloadResult.path);
}
onClose();
} catch (error) {
console.error("Failed to download subtitle:", error);
} finally {
setDownloadingId(null);
}
},
[
downloadAsync,
onServerSubtitleDownloaded,
onLocalSubtitleDownloaded,
onClose,
],
);
const displayLanguages = useMemo(
() => COMMON_SUBTITLE_LANGUAGES.slice(0, 16),
[],
);
const trackOptions = useMemo(() => {
const noneOption = {
label: t("item_card.subtitles.none"),
sublabel: undefined as string | undefined,
value: -1,
selected: currentSubtitleIndex === -1,
};
const options = subtitleTracks.map((track) => ({
label:
track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`,
sublabel: track.Codec?.toUpperCase(),
value: track.Index!,
selected: track.Index === currentSubtitleIndex,
}));
return [noneOption, ...options];
}, [subtitleTracks, currentSubtitleIndex, t]);
if (!visible) return null;
return (
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
<Animated.View
style={[
styles.sheetContainer,
{ transform: [{ translateY: sheetTranslateY }] },
]}
>
<BlurView intensity={90} tint='dark' style={styles.blurContainer}>
<TVFocusGuideView
trapFocusUp
trapFocusDown
trapFocusLeft
trapFocusRight
style={styles.content}
>
{/* Header with tabs */}
<View style={styles.header}>
<Text style={styles.title}>
{t("item_card.subtitles.label") || "Subtitles"}
</Text>
{/* Tab bar */}
<View style={styles.tabRow}>
<TVTabButton
label={t("item_card.subtitles.tracks") || "Tracks"}
active={activeTab === "tracks"}
onSelect={() => setActiveTab("tracks")}
/>
<TVTabButton
label={t("player.download") || "Download"}
active={activeTab === "download"}
onSelect={() => setActiveTab("download")}
/>
</View>
</View>
{/* Tracks Tab Content */}
{activeTab === "tracks" && isTabContentReady && (
<View style={styles.section}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.tracksScroll}
contentContainerStyle={styles.tracksScrollContent}
>
{trackOptions.map((option, index) => (
<TVTrackCard
key={option.value}
ref={
index === initialSelectedTrackIndex
? firstTrackRef
: undefined
}
label={option.label}
sublabel={option.sublabel}
selected={option.selected}
hasTVPreferredFocus={index === initialSelectedTrackIndex}
onPress={() => handleTrackSelect(option.value)}
/>
))}
</ScrollView>
</View>
)}
{/* Download Tab Content */}
{activeTab === "download" && isTabContentReady && (
<>
{/* Language Selector */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
{t("player.language") || "Language"}
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.languageScroll}
contentContainerStyle={styles.languageScrollContent}
>
{displayLanguages.map((lang, index) => (
<TVLanguageCard
key={lang.code}
code={lang.code}
name={lang.name}
selected={selectedLanguage === lang.code}
hasTVPreferredFocus={
index === 0 &&
(!searchResults || searchResults.length === 0)
}
onPress={() => handleLanguageSelect(lang.code)}
/>
))}
</ScrollView>
</View>
{/* Results Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
{t("player.results") || "Results"}
{searchResults && ` (${searchResults.length})`}
</Text>
{/* Loading state */}
{isSearching && (
<View style={styles.loadingContainer}>
<ActivityIndicator size='large' color='#fff' />
<Text style={styles.loadingText}>
{t("player.searching") || "Searching..."}
</Text>
</View>
)}
{/* Error state */}
{searchError && !isSearching && (
<View style={styles.errorContainer}>
<Ionicons
name='alert-circle-outline'
size={32}
color='rgba(255,100,100,0.8)'
/>
<Text style={styles.errorText}>
{t("player.search_failed") || "Search failed"}
</Text>
<Text style={styles.errorHint}>
{!hasOpenSubtitlesApiKey
? t("player.no_subtitle_provider") ||
"No subtitle provider configured on server"
: String(searchError)}
</Text>
</View>
)}
{/* No results */}
{searchResults &&
searchResults.length === 0 &&
!isSearching &&
!searchError && (
<View style={styles.emptyContainer}>
<Ionicons
name='document-text-outline'
size={32}
color='rgba(255,255,255,0.4)'
/>
<Text style={styles.emptyText}>
{t("player.no_subtitles_found") ||
"No subtitles found"}
</Text>
</View>
)}
{/* Results list */}
{searchResults &&
searchResults.length > 0 &&
!isSearching && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.resultsScroll}
contentContainerStyle={styles.resultsScrollContent}
>
{searchResults.map((result, index) => (
<TVSubtitleResultCard
key={result.id}
result={result}
hasTVPreferredFocus={index === 0}
isDownloading={downloadingId === result.id}
onPress={() => handleDownload(result)}
/>
))}
</ScrollView>
)}
</View>
{/* API Key hint if no fallback available */}
{!hasOpenSubtitlesApiKey && (
<View style={styles.apiKeyHint}>
<Ionicons
name='information-circle-outline'
size={16}
color='rgba(255,255,255,0.4)'
/>
<Text style={styles.apiKeyHintText}>
{t("player.add_opensubtitles_key_hint") ||
"Add OpenSubtitles API key in settings for client-side fallback"}
</Text>
</View>
)}
</>
)}
{/* Cancel button */}
{isReady && (
<View style={styles.cancelButtonContainer}>
<TVCancelButton
onPress={onClose}
label={t("common.cancel") || "Cancel"}
/>
</View>
)}
</TVFocusGuideView>
</BlurView>
</Animated.View>
</Animated.View>
);
};
const styles = StyleSheet.create({
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.6)",
justifyContent: "flex-end",
zIndex: 1000,
},
sheetContainer: {
maxHeight: "70%",
},
blurContainer: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: "hidden",
},
content: {
paddingTop: 24,
paddingBottom: 48,
},
header: {
paddingHorizontal: 48,
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: "600",
color: "#fff",
marginBottom: 16,
},
tabRow: {
flexDirection: "row",
gap: 24,
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: "500",
color: "rgba(255,255,255,0.5)",
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: 12,
paddingHorizontal: 48,
},
tracksScroll: {
overflow: "visible",
},
tracksScrollContent: {
paddingHorizontal: 48,
paddingVertical: 8,
gap: 12,
},
languageScroll: {
overflow: "visible",
},
languageScrollContent: {
paddingHorizontal: 48,
paddingVertical: 8,
gap: 10,
},
resultsScroll: {
overflow: "visible",
},
resultsScrollContent: {
paddingHorizontal: 48,
paddingVertical: 8,
gap: 12,
},
loadingContainer: {
paddingVertical: 40,
alignItems: "center",
},
loadingText: {
color: "rgba(255,255,255,0.6)",
marginTop: 12,
fontSize: 14,
},
errorContainer: {
paddingVertical: 40,
paddingHorizontal: 48,
alignItems: "center",
},
errorText: {
color: "rgba(255,100,100,0.9)",
marginTop: 8,
fontSize: 16,
fontWeight: "500",
},
errorHint: {
color: "rgba(255,255,255,0.5)",
marginTop: 4,
fontSize: 13,
textAlign: "center",
},
emptyContainer: {
paddingVertical: 40,
alignItems: "center",
},
emptyText: {
color: "rgba(255,255,255,0.5)",
marginTop: 8,
fontSize: 14,
},
apiKeyHint: {
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingHorizontal: 48,
paddingTop: 8,
},
apiKeyHintText: {
color: "rgba(255,255,255,0.4)",
fontSize: 12,
},
cancelButtonContainer: {
paddingHorizontal: 48,
paddingTop: 20,
alignItems: "flex-start",
},
});

View File

@@ -115,7 +115,7 @@
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.8.3", "react-native-worklets": "0.10.2",
"sonner-native": "0.21.2", "sonner-native": "0.21.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "اختر",
"no_trailer_available": "لا يوجد مقطع دعائي متوفر", "no_trailer_available": "لا يوجد مقطع دعائي متوفر",
"video": "فيديو", "video": "فيديو",
"audio": "الصوت", "audio": "الصوت",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Vídeo", "video": "Vídeo",
"audio": "Àudio", "audio": "Àudio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Vybrat",
"no_trailer_available": "Přípojné vozidlo není k dispozici", "no_trailer_available": "Přípojné vozidlo není k dispozici",
"video": "Video", "video": "Video",
"audio": "Zvuk", "audio": "Zvuk",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Vælg",
"no_trailer_available": "Intet påhængskøretøj tilgængeligt", "no_trailer_available": "Intet påhængskøretøj tilgængeligt",
"video": "Video", "video": "Video",
"audio": "Lyd", "audio": "Lyd",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Auswählen",
"no_trailer_available": "Kein Trailer verfügbar", "no_trailer_available": "Kein Trailer verfügbar",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Sprache", "language": "Sprache",
"results": "Ergebnisse", "results": "Ergebnisse",
"searching": "Suche ...",
"search_failed": "Suche fehlgeschlagen", "search_failed": "Suche fehlgeschlagen",
"no_subtitle_provider": "Kein Untertitelanbieter auf dem Server konfiguriert", "no_subtitle_provider": "Kein Untertitelanbieter auf dem Server konfiguriert",
"no_subtitles_found": "Keine Untertitel gefunden", "no_subtitles_found": "Keine Untertitel gefunden",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Επιλογή",
"no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο", "no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο",
"video": "Βίντεο", "video": "Βίντεο",
"audio": "Ήχος", "audio": "Ήχος",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No results", "no_results": "No results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin server", "using_jellyfin_server": "Using Jellyfin server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Seleccionar",
"no_trailer_available": "No hay tráiler disponible", "no_trailer_available": "No hay tráiler disponible",
"video": "Vídeo", "video": "Vídeo",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Valitse",
"no_trailer_available": "Perävaunua ei saatavilla", "no_trailer_available": "Perävaunua ei saatavilla",
"video": "Video", "video": "Video",
"audio": "Ääni", "audio": "Ääni",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -4,9 +4,9 @@
"error_title": "Erreur", "error_title": "Erreur",
"login_title": "Se connecter", "login_title": "Se connecter",
"login_to_title": "Se connecter à", "login_to_title": "Se connecter à",
"select_user": "Sélectionnez un utilisateur pour vous connecter", "select_user": "Select a user to log in",
"add_user_to_login": "Ajouter un utilisateur pour se connecter", "add_user_to_login": "Add a user to log in",
"add_user": "Ajouter un utilisateur", "add_user": "Add User",
"username_placeholder": "Nom d'utilisateur", "username_placeholder": "Nom d'utilisateur",
"password_placeholder": "Mot de passe", "password_placeholder": "Mot de passe",
"login_button": "Se connecter", "login_button": "Se connecter",
@@ -42,19 +42,19 @@
"please_login_again": "Votre session enregistrée a expiré. Veuillez vous connecter à nouveau.", "please_login_again": "Votre session enregistrée a expiré. Veuillez vous connecter à nouveau.",
"remove_saved_login": "Supprimer l'identifiant enregistré", "remove_saved_login": "Supprimer l'identifiant enregistré",
"remove_saved_login_description": "Cela supprimera vos identifiants enregistrés pour ce serveur. Vous devrez saisir à nouveau votre nom dutilisateur et votre mot de passe la prochaine fois.", "remove_saved_login_description": "Cela supprimera vos identifiants enregistrés pour ce serveur. Vous devrez saisir à nouveau votre nom dutilisateur et votre mot de passe la prochaine fois.",
"accounts_count": "{{count}} comptes", "accounts_count": "Comptes {{count}}",
"select_account": "Sélectionnez un compte", "select_account": "Sélectionnez un compte",
"add_account": "Ajouter un compte", "add_account": "Ajouter un compte",
"remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}.", "remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}.",
"remove_server": "Supprimer le serveur", "remove_server": "Remove Server",
"remove_server_description": "Ceci supprimera {{server}} et tous les comptes enregistrés de votre liste.", "remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Sélectionnez votre serveur", "select_your_server": "Select Your Server",
"add_server_to_get_started": "Ajouter un serveur pour commencer", "add_server_to_get_started": "Add a server to get started",
"add_server": "Ajouter un serveur", "add_server": "Add Server",
"change_server": "Changer de serveur" "change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Enregistrer le compte", "title": "Sauvegarder le compte",
"save_for_later": "Enregistrer ce compte", "save_for_later": "Enregistrer ce compte",
"security_option": "Options de sécurité", "security_option": "Options de sécurité",
"no_protection": "Aucune protection", "no_protection": "Aucune protection",
@@ -95,9 +95,9 @@
"oops": "Oups!", "oops": "Oups!",
"error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.",
"continue_watching": "Continuer à regarder", "continue_watching": "Continuer à regarder",
"continue": "Continuer", "continue": "Continue",
"next_up": "À suivre", "next_up": "À suivre",
"continue_and_next_up": "Continuer à regarder et à suivre", "continue_and_next_up": "Continuer de regarder et à suivre",
"recently_added_in": "Ajoutés récemment dans {{libraryName}}", "recently_added_in": "Ajoutés récemment dans {{libraryName}}",
"suggested_movies": "Films suggérés", "suggested_movies": "Films suggérés",
"suggested_episodes": "Épisodes suggérés", "suggested_episodes": "Épisodes suggérés",
@@ -120,10 +120,10 @@
"settings_title": "Paramètres", "settings_title": "Paramètres",
"log_out_button": "Déconnexion", "log_out_button": "Déconnexion",
"switch_user": { "switch_user": {
"title": "Changer d'utilisateur", "title": "Switch User",
"account": "Compte", "account": "Account",
"switch_user": "Changer d'utilisateur sur ce serveur", "switch_user": "Switch User on This Server",
"current": "actuel" "current": "current"
}, },
"categories": { "categories": {
"title": "Catégories" "title": "Catégories"
@@ -136,32 +136,32 @@
}, },
"appearance": { "appearance": {
"title": "Apparence", "title": "Apparence",
"merge_next_up_continue_watching": "Fusionner « Continuer à regarder » et « À suivre »", "merge_next_up_continue_watching": "Fusionner, continuer à regarder et à suivre",
"hide_remote_session_button": "Masquer le bouton de session distante", "hide_remote_session_button": "Masquer le bouton de session distante",
"show_home_backdrop": "Arrière-plan d'accueil dynamique", "show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Carrousel principal", "show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Afficher l'affiche de la série sur les épisodes", "show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Musique de thème", "theme_music": "Theme Music",
"display_size": "Taille d'affichage", "display_size": "Display Size",
"display_size_small": "Petite", "display_size_small": "Small",
"display_size_default": "Par défaut", "display_size_default": "Default",
"display_size_large": "Grande", "display_size_large": "Large",
"display_size_extra_large": "Très grande" "display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Réseau", "title": "Réseau",
"local_network": "Réseau local", "local_network": "Réseau local",
"auto_switch_enabled": "Basculement automatique à la maison", "auto_switch_enabled": "Basculement automatique quand à la maison",
"auto_switch_description": "Basculer automatiquement vers l'URL locale lorsque vous êtes connecté au Wi-Fi de la maison", "auto_switch_description": "Basculer automatiquement vers l'URL locale lorsque vous êtes connecté au Wi-Fi de la maison",
"local_url": "URL locale", "local_url": "URL locale",
"local_url_hint": "Entrez l'adresse de votre serveur local (par exemple http://192.168.1.100:8096)", "local_url_hint": "Entrez l'adresse de votre serveur local (exemple, http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Réseaux Wi-Fi domestiques", "home_wifi_networks": "Réseaux Wi-Fi domestiques",
"add_current_network": "Ajouter \"{{ssid}}\"", "add_current_network": "Ajouter \"{{ssid}}\"",
"not_connected_to_wifi": "Non connecté au Wi-Fi", "not_connected_to_wifi": "Non connecté au WiFi",
"no_networks_configured": "Pas de réseau configuré", "no_networks_configured": "Pas de réseau configuré",
"add_network_hint": "Ajouter votre réseau Wi-Fi domestique pour activer le basculement automatique", "add_network_hint": "Ajouter votre réseau Wi-Fi domestique pour activer la commutation automatique",
"current_wifi": "Wi-Fi actuel", "current_wifi": "WiFi actuel",
"using_url": "Utilisant", "using_url": "Utilisant",
"local": "URL locale", "local": "URL locale",
"remote": "URL à distance", "remote": "URL à distance",
@@ -172,9 +172,9 @@
"not_configured": "Non configuré", "not_configured": "Non configuré",
"network_added": "Réseau ajouté", "network_added": "Réseau ajouté",
"network_already_added": "Réseau déjà ajouté", "network_already_added": "Réseau déjà ajouté",
"no_wifi_connected": "Non connecté au Wi-Fi", "no_wifi_connected": "Non connecté au WiFi",
"permission_denied": "Autorisation de localisation refusée", "permission_denied": "Autorisation de localisation refusée",
"permission_denied_explanation": "Une autorisation de localisation est requise pour détecter le réseau Wi-Fi afin de basculer automatiquement. Veuillez l'activer dans les paramètres." "permission_denied_explanation": "Une autorisation de localisation est requise pour détecter le réseau Wifi afin de changer automatiquement. Veuillez lactiver dans les paramètres."
}, },
"user_info": { "user_info": {
"user_info_title": "Informations utilisateur", "user_info_title": "Informations utilisateur",
@@ -184,35 +184,35 @@
"app_version": "Version de l'application" "app_version": "Version de l'application"
}, },
"quick_connect": { "quick_connect": {
"quick_connect_title": "Connexion rapide", "quick_connect_title": "Connexion Rapide",
"authorize_button": "Autoriser une connexion rapide", "authorize_button": "Autoriser une Connexion Rapide",
"enter_the_quick_connect_code": "Entrez le code de connexion rapide...", "enter_the_quick_connect_code": "Entrez le code de Connexion Rapide...",
"success": "Succès", "success": "Succès",
"quick_connect_autorized": "Connexion rapide autorisée", "quick_connect_autorized": "Connexion Rapide autorisé",
"error": "Erreur", "error": "Erreur",
"invalid_code": "Code invalide", "invalid_code": "Code invalide",
"authorize": "Autoriser" "authorize": "Autoriser"
}, },
"media_controls": { "media_controls": {
"media_controls_title": "Contrôles média", "media_controls_title": "Contrôles Média",
"forward_skip_length": "Durée de saut en avant", "forward_skip_length": "Durée de saut en avant",
"rewind_length": "Durée de retour en arrière", "rewind_length": "Durée de retour en arrière",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": { "buffer": {
"title": "Paramètres du tampon", "title": "Buffer Settings",
"cache_mode": "Mode de cache", "cache_mode": "Cache Mode",
"cache_auto": "Auto", "cache_auto": "Auto",
"cache_yes": "Activé", "cache_yes": "Enabled",
"cache_no": "Désactivé", "cache_no": "Disabled",
"buffer_duration": "Durée du tampon", "buffer_duration": "Buffer Duration",
"max_cache_size": "Taille maximale du cache", "max_cache_size": "Max Cache Size",
"max_backward_cache": "Cache arrière maximal" "max_backward_cache": "Max Backward Cache"
}, },
"vo_driver": { "vo_driver": {
"title": "Sortie vidéo", "title": "Video Output",
"vo_mode": "Pilote VO", "vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommandé)", "gpu_next": "gpu-next (Recommended)",
"gpu": "gpu" "gpu": "gpu"
}, },
"gesture_controls": { "gesture_controls": {
@@ -225,8 +225,8 @@
"right_side_volume_description": "Glisser vers le haut/bas sur le côté droit pour ajuster le volume", "right_side_volume_description": "Glisser vers le haut/bas sur le côté droit pour ajuster le volume",
"hide_volume_slider": "Masquer le curseur de volume", "hide_volume_slider": "Masquer le curseur de volume",
"hide_volume_slider_description": "Masquer le curseur de volume dans le lecteur vidéo", "hide_volume_slider_description": "Masquer le curseur de volume dans le lecteur vidéo",
"hide_brightness_slider": "Masquer le curseur de luminosité", "hide_brightness_slider": "Cacher le curseur de luminosité",
"hide_brightness_slider_description": "Masquer le curseur de luminosité dans le lecteur vidéo" "hide_brightness_slider_description": "Masquer le curseur de volume dans le lecteur vidéo"
}, },
"audio": { "audio": {
"audio_title": "Audio", "audio_title": "Audio",
@@ -234,14 +234,14 @@
"audio_language": "Langue audio", "audio_language": "Langue audio",
"audio_hint": "Choisissez une langue audio par défaut.", "audio_hint": "Choisissez une langue audio par défaut.",
"none": "Aucune", "none": "Aucune",
"language": "Langue", "language": "Langage",
"transcode_mode": { "transcode_mode": {
"title": "Transcodage audio", "title": "Transcodage audio",
"description": "Contrôle la gestion de l'audio surround (7.1, TrueHD, DTS-HD)", "description": "Contrôle la gestion de l'audio surround (7.1, TrueHD, DTS-HD)",
"auto": "Auto", "auto": "Auto",
"stereo": "Forcer la stéréo", "stereo": "Forcer la stéréo",
"5_1": "Autoriser 5.1", "5_1": "Autoriser 5.1",
"passthrough": "Passthrough" "passthrough": "Intercommunication"
} }
}, },
"subtitles": { "subtitles": {
@@ -252,7 +252,7 @@
"set_subtitle_track": "Piste de sous-titres de l'élément précédent", "set_subtitle_track": "Piste de sous-titres de l'élément précédent",
"subtitle_size": "Taille des sous-titres", "subtitle_size": "Taille des sous-titres",
"none": "Aucune", "none": "Aucune",
"language": "Langue", "language": "Langage",
"loading": "Chargement", "loading": "Chargement",
"modes": { "modes": {
"Default": "Par défaut", "Default": "Par défaut",
@@ -262,20 +262,20 @@
"OnlyForced": "Forcés seulement" "OnlyForced": "Forcés seulement"
}, },
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Entrez votre clé API OpenSubtitles pour activer la recherche de sous-titres côté client comme solution de secours lorsque votre serveur Jellyfin n'a pas de fournisseur de sous-titres configuré.", "opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "Clé API", "opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Entrez la clé API...", "opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Obtenez votre clé API gratuite sur opensubtitles.com/fr/consumers", "opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Échelle des sous-titres", "mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Marge verticale", "mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Alignement horizontal", "mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Alignement vertical", "mpv_subtitle_align_y": "Vertical Align",
"align": { "align": {
"left": "Gauche", "left": "Left",
"center": "Centre", "center": "Center",
"right": "Droite", "right": "Right",
"top": "Haut", "top": "Top",
"bottom": "Bas" "bottom": "Bottom"
} }
}, },
"other": { "other": {
@@ -283,38 +283,38 @@
"video_orientation": "Orientation vidéo", "video_orientation": "Orientation vidéo",
"orientation": "Orientation", "orientation": "Orientation",
"orientations": { "orientations": {
"DEFAULT": "Suivre l'orientation de l'appareil", "DEFAULT": "Par défaut",
"ALL": "Toutes", "ALL": "Toutes",
"PORTRAIT": "Portrait auto", "PORTRAIT": "Portrait",
"PORTRAIT_UP": "Portrait haut", "PORTRAIT_UP": "Portrait Haut",
"PORTRAIT_DOWN": "Portrait bas", "PORTRAIT_DOWN": "Portrait Bas",
"LANDSCAPE": "Paysage auto", "LANDSCAPE": "Paysage",
"LANDSCAPE_LEFT": "Paysage gauche", "LANDSCAPE_LEFT": "Paysage Gauche",
"LANDSCAPE_RIGHT": "Paysage droite", "LANDSCAPE_RIGHT": "Paysage Droite",
"OTHER": "Autre", "OTHER": "Autre",
"UNKNOWN": "Inconnu" "UNKNOWN": "Inconnu"
}, },
"safe_area_in_controls": "Zone de sécurité dans les contrôles", "safe_area_in_controls": "Zone de sécurité dans les contrôles",
"show_custom_menu_links": "Afficher les liens personnalisés", "show_custom_menu_links": "Afficher les liens personnalisés",
"show_large_home_carousel": "Afficher le grand carrousel daccueil (bêta)", "show_large_home_carousel": "Afficher le grand carrousel daccueil (bêta)",
"hide_libraries": "Masquer les bibliothèques", "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.", "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", "disable_haptic_feedback": "Désactiver le retour haptique",
"default_quality": "Qualité par défaut", "default_quality": "Qualité par défaut",
"default_playback_speed": "Vitesse de lecture par défaut", "default_playback_speed": "Vitesse de lecture par défaut",
"auto_play_next_episode": "Lecture automatique de l'épisode suivant", "auto_play_next_episode": "Lecture automatique de l'épisode suivant",
"max_auto_play_episode_count": "Nombre max d'épisodes en lecture automatique", "max_auto_play_episode_count": "Nombre d'épisodes en lecture automatique max",
"disabled": "Désactivé" "disabled": "Désactivé"
}, },
"music": { "music": {
"title": "Musique", "title": "Musique",
"playback_title": "Lecture", "playback_title": "Lecture",
"playback_description": "Configurer le mode de lecture de la musique.", "playback_description": "Configurer le mode de lecture de la musique.",
"prefer_downloaded": "Préférer les musiques téléchargées", "prefer_downloaded": "Supprimer toutes les musiques téléchargées",
"caching_title": "Mise en cache", "caching_title": "Mise en cache",
"caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.", "caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.",
"lookahead_enabled": "Activer la mise en cache anticipée", "lookahead_enabled": "Activer la mise en cache guidée",
"lookahead_count": "Pistes à mettre en cache à l'avance", "lookahead_count": "Pistes à pré-mettre en cache",
"max_cache_size": "Taille max de cache" "max_cache_size": "Taille max de cache"
}, },
"plugins": { "plugins": {
@@ -333,7 +333,7 @@
"tv_quota_days": "Jours de quota de séries", "tv_quota_days": "Jours de quota de séries",
"reset_jellyseerr_config_button": "Réinitialiser la configuration Seerr", "reset_jellyseerr_config_button": "Réinitialiser la configuration Seerr",
"unlimited": "Illimité", "unlimited": "Illimité",
"plus_n_more": "+{{n}} de plus", "plus_n_more": "+{{n}} Plus",
"order_by": { "order_by": {
"DEFAULT": "Par défaut", "DEFAULT": "Par défaut",
"VOTE_COUNT_AND_AVERAGE": "Nombre de votes et moyenne", "VOTE_COUNT_AND_AVERAGE": "Nombre de votes et moyenne",
@@ -341,7 +341,7 @@
} }
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Activer la recherche Marlin", "enable_marlin_search": "Activer Marlin Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://domaine.org:port", "server_url_placeholder": "http(s)://domaine.org:port",
"marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.",
@@ -362,10 +362,10 @@
"features_title": "Fonctionnalités", "features_title": "Fonctionnalités",
"enable_movie_recommendations": "Recommandations de films", "enable_movie_recommendations": "Recommandations de films",
"enable_series_recommendations": "Recommandations de séries", "enable_series_recommendations": "Recommandations de séries",
"enable_promoted_watchlists": "Listes de suivi promues", "enable_promoted_watchlists": "Listes de lecture promues",
"hide_watchlists_tab": "Masquer l'onglet des listes de suivi", "hide_watchlists_tab": "Masquer l'onglet des listes de lecture",
"home_sections_hint": "Afficher des recommandations personnalisées et des listes de suivi promues de Streamystats sur la page daccueil.", "home_sections_hint": "Afficher des recommandations personnalisées et des listes de lecture promues de Streamystats sur la page daccueil.",
"recommended_movies": "Films recommandés", "recommended_movies": "Films Recommandés",
"recommended_series": "Séries recommandées", "recommended_series": "Séries recommandées",
"toasts": { "toasts": {
"saved": "Enregistré", "saved": "Enregistré",
@@ -375,7 +375,7 @@
"refresh_from_server": "Rafraîchir les paramètres depuis le serveur" "refresh_from_server": "Rafraîchir les paramètres depuis le serveur"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Activer l'intégration de la liste de suivi" "watchlist_enabler": "Activer l'intégration de notre liste de lecture"
} }
}, },
"storage": { "storage": {
@@ -392,10 +392,10 @@
"delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées", "delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées",
"downloaded_songs_size": "{{size}} téléchargé", "downloaded_songs_size": "{{size}} téléchargé",
"downloaded_songs_deleted": "Chansons téléchargées supprimées", "downloaded_songs_deleted": "Chansons téléchargées supprimées",
"clear_all_cache": "Effacer tout le cache", "clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Effacer tout le cache ?", "clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Êtes-vous sûr de vouloir effacer toutes les données en cache ? Cela supprimera toutes les images, fichiers musicaux, sous-titres et caches de requêtes mis en cache. Vos paramètres et votre session de connexion seront conservés.", "clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_error_desc": "Une erreur est survenue lors de l'effacement du cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Introduction", "title": "Introduction",
@@ -419,17 +419,17 @@
"error_deleting_files": "Erreur lors de la suppression des fichiers" "error_deleting_files": "Erreur lors de la suppression des fichiers"
}, },
"security": { "security": {
"title": "Sécurité", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Délai d'inactivité", "title": "Inactivity Timeout",
"disabled": "Désactivé", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
"15_minutes": "15 minutes", "15_minutes": "15 minutes",
"30_minutes": "30 minutes", "30_minutes": "30 minutes",
"1_hour": "1 heure", "1_hour": "1 hour",
"4_hours": "4 heures", "4_hours": "4 hours",
"24_hours": "24 heures" "24_hours": "24 hours"
} }
} }
}, },
@@ -446,7 +446,7 @@
"delete_all_movies_button": "Supprimer tous les films", "delete_all_movies_button": "Supprimer tous les films",
"delete_all_series_button": "Supprimer toutes les séries", "delete_all_series_button": "Supprimer toutes les séries",
"delete_all_button": "Supprimer tous les médias", "delete_all_button": "Supprimer tous les médias",
"delete_all_other_media_button": "Supprimer les autres médias", "delete_all_other_media_button": "Supprimer un autre média",
"active_download": "Téléchargement actif", "active_download": "Téléchargement actif",
"no_active_downloads": "Pas de téléchargements actifs", "no_active_downloads": "Pas de téléchargements actifs",
"active_downloads": "Téléchargements actifs", "active_downloads": "Téléchargements actifs",
@@ -454,7 +454,7 @@
"new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.", "new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.",
"back": "Retour", "back": "Retour",
"delete": "Supprimer", "delete": "Supprimer",
"delete_download": "Supprimer le téléchargement", "delete_download": "Delete Download",
"something_went_wrong": "Quelque chose s'est mal passé", "something_went_wrong": "Quelque chose s'est mal passé",
"could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -465,7 +465,7 @@
"deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès!", "deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès!",
"failed_to_delete_all_series": "Échec de la suppression de toutes les séries", "failed_to_delete_all_series": "Échec de la suppression de toutes les séries",
"deleted_media_successfully": "Les autres médias ont été supprimés avec succès !", "deleted_media_successfully": "Les autres médias ont été supprimés avec succès !",
"failed_to_delete_media": "Échec de la suppression des autres médias", "failed_to_delete_media": "Échec de la suppression d'un autre média",
"download_cancelled": "Téléchargement annulé", "download_cancelled": "Téléchargement annulé",
"could_not_delete_download": "Impossible de supprimer le téléchargement", "could_not_delete_download": "Impossible de supprimer le téléchargement",
"download_completed": "Téléchargement terminé", "download_completed": "Téléchargement terminé",
@@ -483,16 +483,17 @@
} }
}, },
"common": { "common": {
"no_results": "Aucun résultat", "no_results": "No Results",
"select": "Sélectionner",
"no_trailer_available": "Aucune bande-annonce disponible", "no_trailer_available": "Aucune bande-annonce disponible",
"video": "Vidéo", "video": "Vidéo",
"audio": "Audio", "audio": "Audio",
"subtitle": "Sous-titres", "subtitle": "Sous-titres",
"play": "Lecture", "play": "Lecture",
"mark_as_played": "Marquer comme vu", "mark_as_played": "Mark as Played",
"mark_as_not_played": "Marquer comme non vu", "mark_as_not_played": "Mark as not Played",
"none": "Aucun", "none": "Aucun",
"track": "Piste", "track": "Suivre",
"cancel": "Annuler", "cancel": "Annuler",
"delete": "Supprimer", "delete": "Supprimer",
"ok": "Ok", "ok": "Ok",
@@ -500,15 +501,15 @@
"back": "Précédent", "back": "Précédent",
"continue": "Continuer", "continue": "Continuer",
"verifying": "Vérification...", "verifying": "Vérification...",
"login": "Connexion", "login": "Login",
"episodes": "Épisodes", "episodes": "Episodes",
"movies": "Films", "movies": "Movies",
"loading": "Chargement…", "loading": "Loading…",
"seeAll": "Tout afficher" "seeAll": "See all"
}, },
"search": { "search": {
"search": "Rechercher...", "search": "Rechercher...",
"x_items": "{{count}} médias", "x_items": "{{count}} Médias",
"library": "Bibliothèque", "library": "Bibliothèque",
"discover": "Découvrir", "discover": "Découvrir",
"no_results": "Aucun résultat", "no_results": "Aucun résultat",
@@ -526,7 +527,7 @@
"request_series": "Demander une série", "request_series": "Demander une série",
"recently_added": "Ajoutés récemment", "recently_added": "Ajoutés récemment",
"recent_requests": "Demandes récentes", "recent_requests": "Demandes récentes",
"plex_watchlist": "Liste de suivi Plex", "plex_watchlist": "Liste de lecture Plex",
"trending": "Tendance", "trending": "Tendance",
"popular_movies": "Films populaires", "popular_movies": "Films populaires",
"movie_genres": "Genres de films", "movie_genres": "Genres de films",
@@ -535,7 +536,7 @@
"popular_tv": "Séries populaires", "popular_tv": "Séries populaires",
"tv_genres": "Genres des séries", "tv_genres": "Genres des séries",
"upcoming_tv": "Séries à venir", "upcoming_tv": "Séries à venir",
"networks": "Chaînes", "networks": "Studios",
"tmdb_movie_keyword": "Mots-clés de film TMDB", "tmdb_movie_keyword": "Mots-clés de film TMDB",
"tmdb_movie_genre": "Genre de film TMDB", "tmdb_movie_genre": "Genre de film TMDB",
"tmdb_tv_keyword": "Mots-clés de séries TMDB", "tmdb_tv_keyword": "Mots-clés de séries TMDB",
@@ -552,7 +553,7 @@
"item_types": { "item_types": {
"movies": "Films", "movies": "Films",
"series": "Séries", "series": "Séries",
"boxsets": "Coffrets", "boxsets": "Coffrets ",
"playlists": "Playlists", "playlists": "Playlists",
"items": "Médias" "items": "Médias"
}, },
@@ -574,10 +575,10 @@
"filter_by": "Filtrer par", "filter_by": "Filtrer par",
"sort_order": "Ordre de tri", "sort_order": "Ordre de tri",
"tags": "Tags", "tags": "Tags",
"all": "Tout", "all": "All",
"reset": "Réinitialiser", "reset": "Reset",
"asc": "Croissant", "asc": "Ascending",
"desc": "Décroissant" "desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -591,11 +592,11 @@
"noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide."
}, },
"custom_links": { "custom_links": {
"no_links": "Aucun lien" "no_links": "Aucuns liens"
}, },
"player": { "player": {
"live": "EN DIRECT", "live": "LIVE",
"mpv_player_title": "Lecteur MPV", "mpv_player_title": "MPV Player",
"error": "Erreur", "error": "Erreur",
"failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux",
"an_error_occured_while_playing_the_video": "Une erreur sest produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.", "an_error_occured_while_playing_the_video": "Une erreur sest produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.",
@@ -610,71 +611,72 @@
"downloaded_file_yes": "Oui", "downloaded_file_yes": "Oui",
"downloaded_file_no": "Non", "downloaded_file_no": "Non",
"downloaded_file_cancel": "Annuler", "downloaded_file_cancel": "Annuler",
"swipe_down_settings": "Balayez vers le bas pour les paramètres", "swipe_down_settings": "Swipe down for settings",
"ends_at": "Se termine à {{time}}", "ends_at": "Ends at {{time}}",
"search_subtitles": "Rechercher des sous-titres", "search_subtitles": "Search Subtitles",
"subtitle_tracks": "Pistes", "subtitle_tracks": "Tracks",
"subtitle_search": "Rechercher et télécharger", "subtitle_search": "Search & Download",
"download": "Télécharger", "download": "Download",
"subtitle_download_hint": "Les sous-titres téléchargés seront enregistrés dans votre bibliothèque", "subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Utilisation du serveur Jellyfin", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Langue", "language": "Language",
"results": "Résultats", "results": "Results",
"search_failed": "Recherche échouée", "searching": "Searching...",
"no_subtitle_provider": "Aucun fournisseur de sous-titres configuré sur le serveur", "search_failed": "Search failed",
"no_subtitles_found": "Aucun sous-titre trouvé", "no_subtitle_provider": "No subtitle provider configured on server",
"add_opensubtitles_key_hint": "Ajoutez une clé API OpenSubtitles dans les paramètres pour une solution de secours côté client", "no_subtitles_found": "No subtitles found",
"settings": "Paramètres", "add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"skip_intro": "Passer l'intro", "settings": "Settings",
"skip_credits": "Passer le générique", "skip_intro": "Skip Intro",
"stopPlayback": "Arrêter la lecture", "skip_credits": "Skip Credits",
"stopPlayingTitle": "Arrêter de lire \"{{title}}\" ?", "stopPlayback": "Stop Playback",
"stopPlayingConfirm": "Êtes-vous sûr de vouloir arrêter la lecture ?", "stopPlayingTitle": "Stop playing \"{{title}}\"?",
"downloaded": "Téléchargé", "stopPlayingConfirm": "Are you sure you want to stop playback?",
"missing_parameters": "Paramètres de lecture manquants" "downloaded": "Downloaded",
"missing_parameters": "Missing playback parameters"
}, },
"chapters": { "chapters": {
"title": "Chapitres", "title": "Chapters",
"chapter_number": "Chapitre {{number}}", "chapter_number": "Chapter {{number}}",
"open": "Ouvrir les chapitres", "open": "Open chapters",
"close": "Fermer les chapitres" "close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "À suivre", "next_up": "À suivre",
"no_items_to_display": "Aucun média à afficher", "no_items_to_display": "Aucuns médias à afficher",
"cast_and_crew": "Distribution et équipe", "cast_and_crew": "Distribution et équipe",
"series": "Séries", "series": "Séries",
"seasons": "Saisons", "seasons": "Saisons",
"season": "Saison", "season": "Saison",
"from_this_series": "De cette série", "from_this_series": "From This Series",
"more_from_this_season": "Plus de cette saison", "more_from_this_season": "More from this Season",
"view_series": "Voir la série", "view_series": "View Series",
"view_season": "Voir la saison", "view_season": "View Season",
"select_season": "Sélectionner une saison", "select_season": "Select Season",
"no_episodes_for_this_season": "Aucun épisode pour cette saison", "no_episodes_for_this_season": "Aucun épisode pour cette saison",
"overview": "Aperçu", "overview": "Aperçu",
"more_with": "Plus avec {{name}}", "more_with": "Plus avec {{name}}",
"similar_items": "Médias similaires", "similar_items": "Médias similaires",
"no_similar_items_found": "Aucun média similaire trouvé", "no_similar_items_found": "Aucuns médias similaires trouvés",
"video": "Vidéo", "video": "Vidéo",
"more_details": "Plus de détails", "more_details": "Plus de détails",
"media_options": "Options média", "media_options": "Options média",
"quality": "Qualité", "quality": "Qualité",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": {
"label": "Sous-titres", "label": "Subtitle",
"none": "Aucun", "none": "None",
"tracks": "Pistes" "tracks": "Tracks"
}, },
"show_more": "Afficher plus", "show_more": "Afficher plus",
"show_less": "Afficher moins", "show_less": "Afficher moins",
"left": "restant", "left": "left",
"director": "Réalisateur", "director": "Director",
"cast": "Distribution", "cast": "Cast",
"technical_details": "Détails techniques", "technical_details": "Technical Details",
"appeared_in": "Apparu dans", "appeared_in": "Apparu dans",
"movies": "Films", "movies": "Movies",
"shows": "Séries", "shows": "Shows",
"could_not_load_item": "Impossible de charger le média", "could_not_load_item": "Impossible de charger le média",
"none": "Aucun", "none": "Aucun",
"download": { "download": {
@@ -686,13 +688,13 @@
"download_unwatched_only": "Non visionné uniquement", "download_unwatched_only": "Non visionné uniquement",
"download_button": "Télécharger" "download_button": "Télécharger"
}, },
"mark_played": "Marquer comme vu", "mark_played": "Mark as Watched",
"mark_unplayed": "Marquer comme non vu", "mark_unplayed": "Mark as Unwatched",
"resume_playback": "Reprendre la lecture", "resume_playback": "Resume Playback",
"resume_playback_description": "Voulez-vous continuer où vous vous êtes arrêté ou commencer à partir du début ?", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Lire depuis le début", "play_from_start": "Play from Start",
"continue_from": "Continuer depuis {{time}}", "continue_from": "Continue from {{time}}",
"no_data_available": "Aucune donnée disponible" "no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Suivant", "next": "Suivant",
@@ -704,16 +706,16 @@
"sports": "Sports", "sports": "Sports",
"for_kids": "Pour enfants", "for_kids": "Pour enfants",
"news": "Actualités", "news": "Actualités",
"page_of": "Page {{current}} sur {{total}}", "page_of": "Page {{current}} of {{total}}",
"no_programs": "Aucun programme disponible", "no_programs": "No programs available",
"no_channels": "Aucune chaîne disponible", "no_channels": "No channels available",
"tabs": { "tabs": {
"programs": "Programmes", "programs": "Programs",
"guide": "Guide", "guide": "Guide",
"channels": "Chaînes", "channels": "Channels",
"recordings": "Enregistrements", "recordings": "Recordings",
"schedule": "Programmation", "schedule": "Schedule",
"series": "Séries" "series": "Series"
} }
}, },
"jellyseerr": { "jellyseerr": {
@@ -723,7 +725,7 @@
"whats_wrong": "Quel est le problème?", "whats_wrong": "Quel est le problème?",
"issue_type": "Type de problème", "issue_type": "Type de problème",
"select_an_issue": "Sélectionnez un problème", "select_an_issue": "Sélectionnez un problème",
"types": "Types", "types": "Types de fichiers",
"describe_the_issue": "(optionnel) Décrivez le problème...", "describe_the_issue": "(optionnel) Décrivez le problème...",
"submit_button": "Soumettre", "submit_button": "Soumettre",
"report_issue_button": "Signaler un problème", "report_issue_button": "Signaler un problème",
@@ -732,7 +734,7 @@
"failed_to_login": "Échec de la connexion", "failed_to_login": "Échec de la connexion",
"cast": "Distribution", "cast": "Distribution",
"details": "Détails", "details": "Détails",
"status": "Statut", "status": "Statuts",
"original_title": "Titre original", "original_title": "Titre original",
"series_type": "Type de série", "series_type": "Type de série",
"release_dates": "Dates de sortie", "release_dates": "Dates de sortie",
@@ -750,32 +752,32 @@
"tags": "Tags", "tags": "Tags",
"quality_profile": "Profil de qualité", "quality_profile": "Profil de qualité",
"root_folder": "Dossier racine", "root_folder": "Dossier racine",
"season_all": "Saison (Toutes)", "season_all": "Saison (Tous)",
"season_number": "Saison {{season_number}}", "season_number": "Saison {{season_number}}",
"number_episodes": "{{episode_number}} épisodes", "number_episodes": "{{episode_number}} Épisodes",
"born": "Né(e) le", "born": "Né(e) le",
"appearances": "Apparitions", "appearances": "Apparences",
"approve": "Valider", "approve": "Valider",
"decline": "Refuser", "decline": "Refuser",
"requested_by": "Demandé par {{user}}", "requested_by": "Demandé par {{user}}",
"unknown_user": "Utilisateur inconnu", "unknown_user": "Utilisateur inconnu",
"select": "Sélectionner", "select": "Select",
"request_all": "Tout demander", "request_all": "Request All",
"request_seasons": "Demander des saisons", "request_seasons": "Request Seasons",
"select_seasons": "Sélectionner les saisons", "select_seasons": "Select Seasons",
"request_selected": "Demander la sélection", "request_selected": "Request Selected",
"n_selected": "{{count}} sélectionnés", "n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", "jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.",
"jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.", "jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.",
"failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Seerr", "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Seerr",
"issue_submitted": "Problème soumis!", "issue_submitted": "Problème soumis!",
"requested_item": "{{item}} demandé !", "requested_item": "{{item}}} demandé!",
"you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander !", "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander !",
"something_went_wrong_requesting_media": "Une erreur s'est produite lors de la demande du média !", "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média !",
"request_approved": "Demande approuvée !", "request_approved": "Demande approuvée !",
"request_declined": "Demande refusée !", "request_declined": "Demande déclinée !",
"failed_to_approve_request": "Échec de l'approbation de la demande", "failed_to_approve_request": "Échec d'approbation de la demande",
"failed_to_decline_request": "Échec du refus de la demande" "failed_to_decline_request": "Échec du refus de la demande"
} }
}, },
@@ -785,7 +787,7 @@
"library": "Bibliothèque", "library": "Bibliothèque",
"custom_links": "Liens personnalisés", "custom_links": "Liens personnalisés",
"favorites": "Favoris", "favorites": "Favoris",
"settings": "Réglages" "settings": "Settings"
}, },
"music": { "music": {
"title": "Musique", "title": "Musique",
@@ -805,13 +807,13 @@
"play_top_tracks": "Jouer les pistes les plus populaires", "play_top_tracks": "Jouer les pistes les plus populaires",
"no_suggestions": "Pas de suggestion disponible", "no_suggestions": "Pas de suggestion disponible",
"no_albums": "Pas d'albums trouvés", "no_albums": "Pas d'albums trouvés",
"no_artists": "Pas d'artistes trouvés", "no_artists": "Pas d'artistes trouvé",
"no_playlists": "Pas de playlists trouvées", "no_playlists": "Pas de playlists trouvées",
"album_not_found": "Album introuvable", "album_not_found": "Album introuvable",
"artist_not_found": "Artiste introuvable", "artist_not_found": "Artiste introuvable",
"playlist_not_found": "Playlist introuvable", "playlist_not_found": "Playlist introuvable",
"track_options": { "track_options": {
"play_next": "Lire ensuite", "play_next": "Lecture suivante",
"add_to_queue": "Ajouter à la file d'attente", "add_to_queue": "Ajouter à la file d'attente",
"add_to_playlist": "Ajouter à la playlist", "add_to_playlist": "Ajouter à la playlist",
"download": "Télécharger", "download": "Télécharger",
@@ -820,15 +822,15 @@
"cached": "En cache", "cached": "En cache",
"delete_download": "Supprimer un téléchargement", "delete_download": "Supprimer un téléchargement",
"delete_cache": "Supprimer du cache", "delete_cache": "Supprimer du cache",
"go_to_artist": "Aller à l'artiste", "go_to_artist": "Voir l'artiste",
"go_to_album": "Aller à l'album", "go_to_album": "Aller à lalbum",
"add_to_favorites": "Ajouter aux favoris", "add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Retirer des favoris", "remove_from_favorites": "Retirer des favoris",
"remove_from_playlist": "Retirer de la playlist" "remove_from_playlist": "Retirer de la playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Créer une playlist", "create_playlist": "Créer une Playlist",
"playlist_name": "Nom de la playlist", "playlist_name": "Nom de la Playlist",
"enter_name": "Entrer le nom de la playlist", "enter_name": "Entrer le nom de la playlist",
"create": "Créer", "create": "Créer",
"search_playlists": "Rechercher des playlists...", "search_playlists": "Rechercher des playlists...",
@@ -839,10 +841,10 @@
"created": "Playlist créée", "created": "Playlist créée",
"create_new": "Créer une nouvelle playlist", "create_new": "Créer une nouvelle playlist",
"failed_to_add": "Échec de l'ajout à la playlist", "failed_to_add": "Échec de l'ajout à la playlist",
"failed_to_remove": "Échec du retrait de la playlist", "failed_to_remove": "Échec de la suppression de la playlist",
"failed_to_create": "Échec de la création de la playlist", "failed_to_create": "Échec de la suppression de la playlist",
"delete_playlist": "Supprimer la playlist", "delete_playlist": "Supprimer la playlist",
"delete_confirm": "Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible.", "delete_confirm": "Êtes-vous sûr de vouloir supprimer « {{ name }} » ? Cette action est irréversible.",
"deleted": "Playlist supprimée", "deleted": "Playlist supprimée",
"failed_to_delete": "Échec de la suppression de la playlist" "failed_to_delete": "Échec de la suppression de la playlist"
}, },
@@ -853,46 +855,46 @@
} }
}, },
"watchlists": { "watchlists": {
"title": "Listes de suivi", "title": "Listes de lecture",
"my_watchlists": "Mes listes de suivi", "my_watchlists": "Mes listes de lecture",
"public_watchlists": "Listes de suivi publiques", "public_watchlists": "Watchlist publique",
"create_title": "Créer une liste de suivi", "create_title": "Créer une Watchlist",
"edit_title": "Modifier la liste de suivi", "edit_title": "Modifier la Watchlist",
"create_button": "Créer une liste de suivi", "create_button": "Créer une Watchlist",
"save_button": "Enregistrer les modifications", "save_button": "Enregistrer les modifications",
"delete_button": "Supprimer", "delete_button": "Supprimer",
"remove_button": "Retirer", "remove_button": "Retirer",
"cancel_button": "Annuler", "cancel_button": "Annuler",
"name_label": "Nom", "name_label": "Nom",
"name_placeholder": "Entrez le nom de la liste de suivi", "name_placeholder": "Entrer le nom de la playlist",
"description_label": "Description", "description_label": "Description",
"description_placeholder": "Entrez la description (facultatif)", "description_placeholder": "Entrez la description (facultatif)",
"is_public_label": "Liste de suivi publique", "is_public_label": "Liste de lecture Publique",
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi", "is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
"allowed_type_label": "Type de contenu", "allowed_type_label": "Type de contenu",
"sort_order_label": "Ordre de tri par défaut", "sort_order_label": "Ordre de tri par défaut",
"empty_title": "Aucune liste de suivi", "empty_title": "Pas de Watchlists",
"empty_description": "Créez votre première liste de suivi pour commencer à organiser vos médias", "empty_description": "Créez votre première liste de suivi pour commencer à organiser vos médias",
"empty_watchlist": "Cette liste de suivi est vide", "empty_watchlist": "Cette liste de suivi est vide",
"empty_watchlist_hint": "Ajouter des éléments de votre bibliothèque à cette liste de suivi", "empty_watchlist_hint": "Ajouter des éléments de votre bibliothèque à cette liste de suivi",
"not_configured_title": "Streamystats non configuré", "not_configured_title": "Streamystats non configuré",
"not_configured_description": "Configurer Streamystats dans les paramètres pour utiliser les listes de suivi", "not_configured_description": "Configurer Streamystats dans les paramètres pour utiliser les listes de suivi",
"go_to_settings": "Accédez aux paramètres", "go_to_settings": "Accédez aux Paramètres",
"add_to_watchlist": "Ajouter à la liste de suivi", "add_to_watchlist": "Ajouter à la Watchlist",
"remove_from_watchlist": "Retirer de la liste de suivi", "remove_from_watchlist": "Retirer de la Watchlist",
"select_watchlist": "Sélectionner la liste de suivi", "select_watchlist": "Sélectionner la liste de suivi",
"create_new": "Créer une nouvelle liste de suivi", "create_new": "Créer une Watchlist",
"item": "élément", "item": "médias",
"items": "éléments", "items": "élément",
"public": "Publique", "public": "Publique",
"private": "Privée", "private": "Privée",
"you": "Vous", "you": "Vous-même",
"by_owner": "Par un autre utilisateur", "by_owner": "Par un autre utilisateur",
"not_found": "Liste de suivi introuvable", "not_found": "Playlist introuvable",
"delete_confirm_title": "Supprimer la liste de suivi", "delete_confirm_title": "Supprimer la Watchlist",
"delete_confirm_message": "Voulez-vous vraiment supprimer « {{name}} » ? Cette action est irréversible.", "delete_confirm_message": "Tous les médias (par défaut)",
"remove_item_title": "Retirer de la liste de suivi", "remove_item_title": "Retirer de la Watchlist",
"remove_item_message": "Retirer « {{name}} » de cette liste de suivi ?", "remove_item_message": "Retirer «{{name}}» de cette liste de suivi?",
"loading": "Chargement des listes de suivi...", "loading": "Chargement des listes de suivi...",
"no_compatible_watchlists": "Aucune liste de suivi compatible", "no_compatible_watchlists": "Aucune liste de suivi compatible",
"create_one_first": "Créer une liste de suivi qui accepte ce type de contenu" "create_one_first": "Créer une liste de suivi qui accepte ce type de contenu"
@@ -908,33 +910,33 @@
} }
}, },
"companion_login": { "companion_login": {
"title": "Associer à la TV", "title": "Pair with TV",
"align_qr": "Alignez le code QR dans le cadre", "align_qr": "Align the QR code within the frame",
"enter_code_manually": "Saisir le code manuellement", "enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Entrez les identifiants pour le téléviseur", "pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Code d'appairage", "pairing_code_label": "Pairing code",
"server": "Serveur", "server": "Server",
"authorize_button": "Autoriser", "authorize_button": "Authorize",
"authorizing": "Autorisation en cours...", "authorizing": "Authorizing...",
"scan_again": "Scanner à nouveau", "scan_again": "Scan Again",
"done": "Terminé", "done": "Done",
"success_title": "Autorisation envoyée", "success_title": "Authorization Sent",
"pairing_tv_connecting": "La TV se connecte à votre compte", "pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Échec de l'autorisation", "error_title": "Authorization Failed",
"error_invalid_qr": "Code QR invalide. Veuillez scanner le code d'appairage du téléviseur.", "error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Une erreur s'est produite. Veuillez réessayer.", "error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "L'accès à la caméra est nécessaire pour scanner les QR codes.", "error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Se connecter en tant que {{username}} ?", "login_as": "Log in as {{username}}?",
"on_server": "sur {{server}}", "on_server": "on {{server}}",
"use_different_user": "Utiliser un autre utilisateur", "use_different_user": "Use a different user",
"open_settings": "Ouvrir les paramètres" "open_settings": "Open Settings"
}, },
"pairing": { "pairing": {
"pair_with_phone": "Associer au téléphone", "pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Se connecter sur la TV", "pair_with_phone_title": "Login TV",
"waiting_for_phone": "En attente du téléphone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scanner avec l'application Streamyfin sur votre téléphone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Connexion en cours...", "logging_in": "Logging in...",
"logging_in_description": "Connexion à votre serveur" "logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "בחר",
"no_trailer_available": "אין טריילר זמין", "no_trailer_available": "אין טריילר זמין",
"video": "וידאו", "video": "וידאו",
"audio": "שמע", "audio": "שמע",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Videó", "video": "Videó",
"audio": "Hang", "audio": "Hang",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -226,7 +226,7 @@
"hide_volume_slider": "Hide Volume Slider", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Nascondi il cursore del volume nel lettore video", "hide_volume_slider_description": "Nascondi il cursore del volume nel lettore video",
"hide_brightness_slider": "Hide Brightness Slider", "hide_brightness_slider": "Hide Brightness Slider",
"hide_brightness_slider_description": "Nascondi il cursore della luminosità nel lettore video" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "Audio", "audio_title": "Audio",
@@ -237,10 +237,10 @@
"language": "Lingua", "language": "Lingua",
"transcode_mode": { "transcode_mode": {
"title": "Audio Transcoding", "title": "Audio Transcoding",
"description": "Controlla come viene gestito l'audio surround (7.1, TrueHD, DTS-HD)", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Automatico", "auto": "Auto",
"stereo": "Force Stereo", "stereo": "Force Stereo",
"5_1": "Consenti 5.1", "5_1": "Allow 5.1",
"passthrough": "Passthrough" "passthrough": "Passthrough"
} }
}, },
@@ -262,20 +262,20 @@
"OnlyForced": "Solo forzati" "OnlyForced": "Solo forzati"
}, },
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Inserisci la tua chiave API OpenSubtitles per abilitare la ricerca dei sottotitoli quando il tuo server Jellyfin non ha un provider di sottotitoli configurato.", "opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key", "opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Inserisci la chiave API...", "opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Ottieni la tua chiave API gratuita su opensubtitles.com/en/consumers", "opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale", "mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin", "mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align", "mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align", "mpv_subtitle_align_y": "Vertical Align",
"align": { "align": {
"left": "Sinistra", "left": "Left",
"center": "Centro", "center": "Center",
"right": "Destra", "right": "Right",
"top": "Alto", "top": "Top",
"bottom": "Basso" "bottom": "Bottom"
} }
}, },
"other": { "other": {
@@ -307,9 +307,9 @@
"disabled": "Disabilitato" "disabled": "Disabilitato"
}, },
"music": { "music": {
"title": "Musica", "title": "Music",
"playback_title": "Riproduzione", "playback_title": "Playback",
"playback_description": "Configura come viene riprodotta la musica.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Prefer Downloaded Songs", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Caching", "caching_title": "Caching",
"caching_description": "Automatically cache upcoming tracks for smoother playback.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
@@ -333,7 +333,7 @@
"tv_quota_days": "Giorni di quota per le serie TV", "tv_quota_days": "Giorni di quota per le serie TV",
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
"unlimited": "Illimitato", "unlimited": "Illimitato",
"plus_n_more": "+{{n}} altro", "plus_n_more": "+{{n}} more",
"order_by": { "order_by": {
"DEFAULT": "Predefinito", "DEFAULT": "Predefinito",
"VOTE_COUNT_AND_AVERAGE": "Conteggio delle votazioni e media", "VOTE_COUNT_AND_AVERAGE": "Conteggio delle votazioni e media",
@@ -352,25 +352,25 @@
} }
}, },
"streamystats": { "streamystats": {
"disable_streamystats": "Disabilita Streamystats", "disable_streamystats": "Disable Streamystats",
"enable_search": "Use for Search", "enable_search": "Use for Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Inserisci l'URL per il tuo server Streamystats. L'URL dovrebbe includere http o https ed eventualmente la porta.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Read More About Streamystats.", "read_more_about_streamystats": "Read More About Streamystats.",
"save": "Salva", "save": "Save",
"features_title": "Funzionalità", "features_title": "Features",
"enable_movie_recommendations": "Movie Recommendations", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "Series Recommendations", "enable_series_recommendations": "Series Recommendations",
"enable_promoted_watchlists": "Promoted Watchlists", "enable_promoted_watchlists": "Promoted Watchlists",
"hide_watchlists_tab": "Hide Watchlists Tab", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Mostra consigli personalizzati e watchlist promosse da Streamystats nella home page.", "home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
"recommended_movies": "Recommended Movies", "recommended_movies": "Recommended Movies",
"recommended_series": "Recommended Series", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "Salvato", "saved": "Saved",
"refreshed": "Impostazioni aggiornate dal server", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats disabilitato" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
@@ -385,17 +385,17 @@
"size_used": "{{used}} di {{total}} usato", "size_used": "{{used}} di {{total}} usato",
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati", "delete_all_downloaded_files": "Cancella Tutti i File Scaricati",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Precarica automaticamente i brani mentre ascolti per una riproduzione più fluida e il supporto offline", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} nella cache", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Cache musicale cancellata", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} scaricato", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Brani scaricati eliminati", "downloaded_songs_deleted": "Downloaded songs deleted",
"clear_all_cache": "Clear All Cache", "clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?", "clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Sei sicuro di voler cancellare tutti i dati nella cache? Questo cancellerà tutte le immagini nella cache, i file musicali, i sottotitoli e le cache delle interrogazioni. Le impostazioni e la sessione di login verranno mantenute.", "clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_error_desc": "Si è verificato un errore durante la cancellazione della cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -404,8 +404,8 @@
}, },
"logs": { "logs": {
"logs_title": "Log", "logs_title": "Log",
"export_logs": "Esporta i logs", "export_logs": "Export logs",
"click_for_more_info": "Clicca per maggiori informazioni", "click_for_more_info": "Click for more info",
"level": "Livello", "level": "Livello",
"no_logs_available": "Nessun log disponibile", "no_logs_available": "Nessun log disponibile",
"delete_all_logs": "Cancella tutti i log" "delete_all_logs": "Cancella tutti i log"
@@ -419,17 +419,17 @@
"error_deleting_files": "Errore nella cancellazione dei file" "error_deleting_files": "Errore nella cancellazione dei file"
}, },
"security": { "security": {
"title": "Sicurezza", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"disabled": "Disabilitato", "disabled": "Disabled",
"1_minute": "1 minuto", "1_minute": "1 minute",
"5_minutes": "5 minuti", "5_minutes": "5 minutes",
"15_minutes": "15 minuti", "15_minutes": "15 minutes",
"30_minutes": "30 minuti", "30_minutes": "30 minutes",
"1_hour": "1 ora", "1_hour": "1 hour",
"4_hours": "4 ore", "4_hours": "4 hours",
"24_hours": "24 ore" "24_hours": "24 hours"
} }
} }
}, },
@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Seleziona",
"no_trailer_available": "Nessun trailer disponibile", "no_trailer_available": "Nessun trailer disponibile",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -493,18 +494,18 @@
"mark_as_not_played": "Mark as not Played", "mark_as_not_played": "Mark as not Played",
"none": "Nulla", "none": "Nulla",
"track": "Traccia", "track": "Traccia",
"cancel": "Annulla", "cancel": "Cancel",
"delete": "Cancella", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Rimuovi", "remove": "Remove",
"back": "Indietro", "back": "Back",
"continue": "Continua", "continue": "Continue",
"verifying": "Verifica in corso...", "verifying": "Verifying...",
"login": "Accedi", "login": "Login",
"episodes": "Episodi", "episodes": "Episodes",
"movies": "Film", "movies": "Movies",
"loading": "Caricamento…", "loading": "Loading…",
"seeAll": "Visualizza tutti" "seeAll": "See all"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -518,10 +519,10 @@
"episodes": "Episodi", "episodes": "Episodi",
"collections": "Collezioni", "collections": "Collezioni",
"actors": "Attori", "actors": "Attori",
"artists": "Artisti", "artists": "Artists",
"albums": "Album", "albums": "Albums",
"songs": "Tracce", "songs": "Songs",
"playlists": "Playlist", "playlists": "Playlists",
"request_movies": "Film Richiesti", "request_movies": "Film Richiesti",
"request_series": "Serie Richieste", "request_series": "Serie Richieste",
"recently_added": "Aggiunti di Recente", "recently_added": "Aggiunti di Recente",
@@ -553,7 +554,7 @@
"movies": "film", "movies": "film",
"series": "serie TV", "series": "serie TV",
"boxsets": "cofanetti", "boxsets": "cofanetti",
"playlists": "Playlist", "playlists": "Playlists",
"items": "elementi" "items": "elementi"
}, },
"options": { "options": {
@@ -565,7 +566,7 @@
"cover": "Copertina", "cover": "Copertina",
"show_titles": "Mostra titoli", "show_titles": "Mostra titoli",
"show_stats": "Mostra statistiche", "show_stats": "Mostra statistiche",
"options_title": "Impostazioni" "options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Generi", "genres": "Generi",
@@ -574,10 +575,10 @@
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Criterio di ordinamento", "sort_order": "Criterio di ordinamento",
"tags": "Tag", "tags": "Tag",
"all": "Tutto", "all": "All",
"reset": "Ripristina", "reset": "Reset",
"asc": "Crescente", "asc": "Ascending",
"desc": "Decrescente" "desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -594,7 +595,7 @@
"no_links": "Nessun link" "no_links": "Nessun link"
}, },
"player": { "player": {
"live": "IN DIRETTA", "live": "LIVE",
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "Errore", "error": "Errore",
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
@@ -605,39 +606,40 @@
"next_episode": "Prossimo Episodio", "next_episode": "Prossimo Episodio",
"continue_watching": "Continua a guardare", "continue_watching": "Continua a guardare",
"go_back": "Indietro", "go_back": "Indietro",
"downloaded_file_title": "Questo file è stato scaricato", "downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Vuoi riprodurre il file scaricato?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Si", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Annulla", "downloaded_file_cancel": "Cancel",
"swipe_down_settings": "Scorri in basso per le impostazioni", "swipe_down_settings": "Swipe down for settings",
"ends_at": "Termina alle {{time}}", "ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles", "search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracce", "subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download", "subtitle_search": "Search & Download",
"download": "Scarica", "download": "Download",
"subtitle_download_hint": "I sottotitoli scaricati verranno salvati nella tua libreria", "subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Lingua", "language": "Language",
"results": "Risultati", "results": "Results",
"search_failed": "Ricerca fallita", "searching": "Searching...",
"no_subtitle_provider": "Nessun provider di sottotitoli configurato sul server", "search_failed": "Search failed",
"no_subtitles_found": "Nessun sottotitolo trovato", "no_subtitle_provider": "No subtitle provider configured on server",
"add_opensubtitles_key_hint": "Aggiungi la chiave API OpenSubtitles nelle impostazioni", "no_subtitles_found": "No subtitles found",
"settings": "Impostazioni", "add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro", "skip_intro": "Skip Intro",
"skip_credits": "Skip Credits", "skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback", "stopPlayback": "Stop Playback",
"stopPlayingTitle": "Interrompere la riproduzione \"{{title}}\"?", "stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Sei sicuro di voler interrompere la riproduzione?", "stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Scaricato", "downloaded": "Downloaded",
"missing_parameters": "Parametri di riproduzione mancanti" "missing_parameters": "Missing playback parameters"
}, },
"chapters": { "chapters": {
"title": "Capitoli", "title": "Chapters",
"chapter_number": "Capitolo {{number}}", "chapter_number": "Chapter {{number}}",
"open": "Apri capitoli", "open": "Open chapters",
"close": "Chiudi i capitoli" "close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Il prossimo", "next_up": "Il prossimo",
@@ -662,19 +664,19 @@
"quality": "Qualità", "quality": "Qualità",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": {
"label": "Sottotitoli", "label": "Subtitle",
"none": "Vuoto", "none": "None",
"tracks": "Tracce" "tracks": "Tracks"
}, },
"show_more": "Mostra di più", "show_more": "Mostra di più",
"show_less": "Mostra di meno", "show_less": "Mostra di meno",
"left": "sinistra", "left": "left",
"director": "Regista", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
"appeared_in": "Apparso in", "appeared_in": "Apparso in",
"movies": "Film", "movies": "Movies",
"shows": "Serie", "shows": "Shows",
"could_not_load_item": "Impossibile caricare l'elemento", "could_not_load_item": "Impossibile caricare l'elemento",
"none": "Nessuno", "none": "Nessuno",
"download": { "download": {
@@ -689,10 +691,10 @@
"mark_played": "Mark as Watched", "mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched", "mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback", "resume_playback": "Resume Playback",
"resume_playback_description": "Vuoi continuare da dove hai lasciato o riniziare da capo?", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start", "play_from_start": "Play from Start",
"continue_from": "Continua da {{time}}", "continue_from": "Continue from {{time}}",
"no_data_available": "Nessun dato disponibile" "no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Prossimo", "next": "Prossimo",
@@ -704,16 +706,16 @@
"sports": "Sport", "sports": "Sport",
"for_kids": "Per Bambini", "for_kids": "Per Bambini",
"news": "Notiziari", "news": "Notiziari",
"page_of": "Pagina {{current}} di {{total}}", "page_of": "Page {{current}} of {{total}}",
"no_programs": "Nessun programma disponibile", "no_programs": "No programs available",
"no_channels": "Nessun canale disponibile", "no_channels": "No channels available",
"tabs": { "tabs": {
"programs": "Programmi", "programs": "Programs",
"guide": "Guida", "guide": "Guide",
"channels": "Canali", "channels": "Channels",
"recordings": "Registrazioni", "recordings": "Recordings",
"schedule": "Pianifica", "schedule": "Schedule",
"series": "Serie Tv" "series": "Series"
} }
}, },
"jellyseerr": { "jellyseerr": {
@@ -759,12 +761,12 @@
"decline": "Rifiuta", "decline": "Rifiuta",
"requested_by": "Richiesto da {{user}}", "requested_by": "Richiesto da {{user}}",
"unknown_user": "Utente Sconosciuto", "unknown_user": "Utente Sconosciuto",
"select": "Seleziona", "select": "Select",
"request_all": "Request All", "request_all": "Request All",
"request_seasons": "Request Seasons", "request_seasons": "Request Seasons",
"select_seasons": "Select Seasons", "select_seasons": "Select Seasons",
"request_selected": "Request Selected", "request_selected": "Request Selected",
"n_selected": "{{count}} selezionati", "n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
@@ -785,39 +787,39 @@
"library": "Libreria", "library": "Libreria",
"custom_links": "Collegamenti personalizzati", "custom_links": "Collegamenti personalizzati",
"favorites": "Preferiti", "favorites": "Preferiti",
"settings": "Impostazioni" "settings": "Settings"
}, },
"music": { "music": {
"title": "Musica", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Suggerimenti", "suggestions": "Suggestions",
"albums": "Album", "albums": "Albums",
"artists": "Artisti", "artists": "Artists",
"playlists": "Playlist", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Riproduci", "play": "Play",
"shuffle": "Riproduzione casuale", "shuffle": "Shuffle",
"play_top_tracks": "Play Top Tracks", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Nessun suggerimento disponibile", "no_suggestions": "No suggestions available",
"no_albums": "Nessun album trovato", "no_albums": "No albums found",
"no_artists": "Artista non trovato", "no_artists": "No artists found",
"no_playlists": "Nessuna playlist trovata", "no_playlists": "No playlists found",
"album_not_found": "Album non trovato", "album_not_found": "Album not found",
"artist_not_found": "Artista non trovato", "artist_not_found": "Artist not found",
"playlist_not_found": "Playlist non trovata", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Play Next", "play_next": "Play Next",
"add_to_queue": "Add to Queue", "add_to_queue": "Add to Queue",
"add_to_playlist": "Add to Playlist", "add_to_playlist": "Add to Playlist",
"download": "Scarica", "download": "Download",
"downloaded": "Scaricato", "downloaded": "Downloaded",
"downloading": "Scaricamento...", "downloading": "Downloading...",
"cached": "Memorizzato nella cache", "cached": "Cached",
"delete_download": "Delete Download", "delete_download": "Delete Download",
"delete_cache": "Remove from Cache", "delete_cache": "Remove from Cache",
"go_to_artist": "Go to Artist", "go_to_artist": "Go to Artist",
@@ -829,112 +831,112 @@
"playlists": { "playlists": {
"create_playlist": "Create Playlist", "create_playlist": "Create Playlist",
"playlist_name": "Playlist Name", "playlist_name": "Playlist Name",
"enter_name": "Inserisci il nome della playlist", "enter_name": "Enter playlist name",
"create": "Crea", "create": "Create",
"search_playlists": "Cerca playlist...", "search_playlists": "Search playlists...",
"added_to": "Aggiunto a {{name}}", "added_to": "Added to {{name}}",
"added": "Aggiunto alla playlist", "added": "Added to playlist",
"removed_from": "Rimosso da {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Rimosso dalla playlist", "removed": "Removed from playlist",
"created": "Playlist creata", "created": "Playlist created",
"create_new": "Create New Playlist", "create_new": "Create New Playlist",
"failed_to_add": "Impossibile aggiungere alla playlist", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Impossibile rimuovere dalla playlist", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Impossibile creare la playlist", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Delete Playlist", "delete_playlist": "Delete Playlist",
"delete_confirm": "Sei sicuro di voler eliminare\"{{name}}\"? Questa azione non può essere annullata.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Playlist eliminata", "deleted": "Playlist deleted",
"failed_to_delete": "Impossibile eliminare la playlist" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Sort By", "title": "Sort By",
"alphabetical": "Alfabetico", "alphabetical": "Alphabetical",
"date_created": "Date Created" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Da vedere", "title": "Watchlists",
"my_watchlists": "My Watchlists", "my_watchlists": "My Watchlists",
"public_watchlists": "Public Watchlists", "public_watchlists": "Public Watchlists",
"create_title": "Create Watchlist", "create_title": "Create Watchlist",
"edit_title": "Edit Watchlist", "edit_title": "Edit Watchlist",
"create_button": "Create Watchlist", "create_button": "Create Watchlist",
"save_button": "Save Changes", "save_button": "Save Changes",
"delete_button": "Cancella", "delete_button": "Delete",
"remove_button": "Rimuovi", "remove_button": "Remove",
"cancel_button": "Annulla", "cancel_button": "Cancel",
"name_label": "Nome", "name_label": "Name",
"name_placeholder": "Inserisci il nome della lista \"Da vedere\"", "name_placeholder": "Enter watchlist name",
"description_label": "Descrizione", "description_label": "Description",
"description_placeholder": "Inserisci descrizione (opzionale)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Public Watchlist", "is_public_label": "Public Watchlist",
"is_public_description": "Permetti ad altri di vedere questa lista", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Content Type", "allowed_type_label": "Content Type",
"sort_order_label": "Default Sort Order", "sort_order_label": "Default Sort Order",
"empty_title": "No Watchlists", "empty_title": "No Watchlists",
"empty_description": "Crea la tua prima lista \"Da vedere\" per iniziare a organizzare i tuoi media", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Questa lista è vuota", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Aggiungi elementi dalla tua libreria a questa lista", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats Not Configured", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Configura Streamystats nelle impostazioni per utilizzare le watchlist", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Vai alle impostazioni", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Add to Watchlist", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "Remove from Watchlist", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Select Watchlist", "select_watchlist": "Select Watchlist",
"create_new": "Create New Watchlist", "create_new": "Create New Watchlist",
"item": "elemento", "item": "item",
"items": "elementi", "items": "items",
"public": "Pubblico", "public": "Public",
"private": "Privato", "private": "Private",
"you": "Tu", "you": "You",
"by_owner": "Da un altro utente", "by_owner": "By another user",
"not_found": "\"Da vedere\" non trovata", "not_found": "Watchlist not found",
"delete_confirm_title": "Delete Watchlist", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Sei sicuro di voler eliminare\"{{name}}\"? Questa azione non può essere annullata.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Remove from Watchlist", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "Rimuovere \"{{name}}\" da questa lista?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Caricamento liste...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Nessuna lista compatibile", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Crea una lista che accetti questo tipo di contenuto" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Playback Speed", "title": "Playback Speed",
"apply_to": "Apply To", "apply_to": "Apply To",
"speed": "Velocità", "speed": "Speed",
"scope": { "scope": {
"media": "Solo questo media", "media": "This media only",
"show": "Questo show", "show": "This show",
"all": "Tutti i media (predefinito)" "all": "All media (default)"
} }
}, },
"companion_login": { "companion_login": {
"title": "Associa con la TV", "title": "Pair with TV",
"align_qr": "Allinea il QR code all'interno del riquadro", "align_qr": "Align the QR code within the frame",
"enter_code_manually": "Inserisci il codice manualmente", "enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Inserire le credenziali per la TV", "pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Codice di associazione", "pairing_code_label": "Pairing code",
"server": "Server", "server": "Server",
"authorize_button": "Autorizza", "authorize_button": "Authorize",
"authorizing": "Autorizzando...", "authorizing": "Authorizing...",
"scan_again": "Scan Again", "scan_again": "Scan Again",
"done": "Fatto", "done": "Done",
"success_title": "Authorization Sent", "success_title": "Authorization Sent",
"pairing_tv_connecting": "La TV si sta collegando al tuo account", "pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed", "error_title": "Authorization Failed",
"error_invalid_qr": "QR code non valido. Scansiona il codice di associazione della TV.", "error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Si è verificato un errore. Riprova.", "error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Per scansionare i codici QR è necessaria l'autorizzazione della fotocamera.", "error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Accedi come {{username}}?", "login_as": "Log in as {{username}}?",
"on_server": "su {{server}}", "on_server": "on {{server}}",
"use_different_user": "Usa un altro utente", "use_different_user": "Use a different user",
"open_settings": "Apri le impostazioni" "open_settings": "Open Settings"
}, },
"pairing": { "pairing": {
"pair_with_phone": "Pair with Phone", "pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV", "pair_with_phone_title": "Login TV",
"waiting_for_phone": "In attesa del telefono...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scansiona con l'applicazione Streamyfin sul tuo telefono", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Accesso in corso...", "logging_in": "Logging in...",
"logging_in_description": "Sto connettendo al server" "logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "選択",
"no_trailer_available": "トレーラーがありません", "no_trailer_available": "トレーラーがありません",
"video": "映像", "video": "映像",
"audio": "音声", "audio": "音声",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Selecteren",
"no_trailer_available": "Geen trailer beschikbaar", "no_trailer_available": "Geen trailer beschikbaar",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Velg",
"no_trailer_available": "Ingen trailer tilgjengelig", "no_trailer_available": "Ingen trailer tilgjengelig",
"video": "Video", "video": "Video",
"audio": "Lyd", "audio": "Lyd",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Wybierz",
"no_trailer_available": "Brak dostępnego zwiastunu", "no_trailer_available": "Brak dostępnego zwiastunu",
"video": "Wideo", "video": "Wideo",
"audio": "Dźwięk", "audio": "Dźwięk",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Selecionar",
"no_trailer_available": "Nenhum trailer disponível", "no_trailer_available": "Nenhum trailer disponível",
"video": "Vídeo", "video": "Vídeo",
"audio": "Áudio", "audio": "Áudio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Selectare",
"no_trailer_available": "Nicio remorcă disponibilă", "no_trailer_available": "Nicio remorcă disponibilă",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Выбрать",
"no_trailer_available": "Трейлер недоступен", "no_trailer_available": "Трейлер недоступен",
"video": "Видео", "video": "Видео",
"audio": "Звук", "audio": "Звук",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "Inga resultat", "no_results": "Inga resultat",
"select": "Välj",
"no_trailer_available": "Ingen trailer tillgänglig", "no_trailer_available": "Ingen trailer tillgänglig",
"video": "Video", "video": "Video",
"audio": "Ljud", "audio": "Ljud",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Använder Jellyfin-server", "using_jellyfin_server": "Använder Jellyfin-server",
"language": "Språk", "language": "Språk",
"results": "Resultat", "results": "Resultat",
"searching": "Söker...",
"search_failed": "Sökningen misslyckades", "search_failed": "Sökningen misslyckades",
"no_subtitle_provider": "Ingen undertextleverantör konfigurerad på servern", "no_subtitle_provider": "Ingen undertextleverantör konfigurerad på servern",
"no_subtitles_found": "Inga undertexter hittades", "no_subtitles_found": "Inga undertexter hittades",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "mu'tlhegh", "video": "mu'tlhegh",
"audio": "QoQ", "audio": "QoQ",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Seç",
"no_trailer_available": "Fragman mevcut değil", "no_trailer_available": "Fragman mevcut değil",
"video": "Video", "video": "Video",
"audio": "Ses", "audio": "Ses",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Âm thanh", "audio": "Âm thanh",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -484,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -620,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -449,6 +449,11 @@ export const pluginSettingsAtom = atom<PluginLockableSettings | undefined>(
const hasMeaningfulSettingValue = (value: unknown) => const hasMeaningfulSettingValue = (value: unknown) =>
value !== undefined && value !== null && value !== ""; value !== undefined && value !== null && value !== "";
const getEffectiveSettingValue = <K extends keyof Settings>(
settings: Partial<Settings> | null | undefined,
settingsKey: K,
) => settings?.[settingsKey] ?? defaultValues[settingsKey];
export const useSettings = () => { export const useSettings = () => {
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const [_settings, setSettings] = useAtom(settingsAtom); const [_settings, setSettings] = useAtom(settingsAtom);
@@ -505,17 +510,7 @@ export const useSettings = () => {
if (!_settings) { if (!_settings) {
return; return;
} }
// Admin-locked settings are enforced at write time too: a control that const hasChanges = Object.entries(update).some(
// isn't disabled in the UI must not persist a value the admin pinned.
// The read memo already overrides locked keys, but without this guard the
// write would silently land in user storage and resurface once unlocked.
const sanitizedUpdate = Object.fromEntries(
Object.entries(update).filter(
([key]) => pluginSettings?.[key as keyof Settings]?.locked !== true,
),
) as Partial<Settings>;
const hasChanges = Object.entries(sanitizedUpdate).some(
([key, value]) => _settings[key as keyof Settings] !== value, ([key, value]) => _settings[key as keyof Settings] !== value,
); );
@@ -524,7 +519,7 @@ export const useSettings = () => {
const newSettings = { const newSettings = {
...defaultValues, ...defaultValues,
..._settings, ..._settings,
...sanitizedUpdate, ...update,
} as Settings; } as Settings;
setSettings(newSettings); setSettings(newSettings);
saveSettings(newSettings); saveSettings(newSettings);
@@ -547,24 +542,13 @@ export const useSettings = () => {
// Normalize object-typed settings from plugin (plain primitive → { key, value }) // Normalize object-typed settings from plugin (plain primitive → { key, value })
value = normalizePluginValue(settingsKey, value); value = normalizePluginValue(settingsKey, value);
// When unlocked, keep the user's value only if they explicitly diverged const effectiveValue = getEffectiveSettingValue(_settings, settingsKey);
// from the app default. Otherwise the plugin value is the admin's
// default and must win over the hardcoded app default — e.g. a toggle
// that was always locked then unlocked should reflect the plugin
// default, not the app's `false`. Object-typed settings compare by
// reference, so their behaviour is unchanged.
const userValue = _settings?.[settingsKey];
const userDiverged =
hasMeaningfulSettingValue(userValue) &&
userValue !== defaultValues[settingsKey];
(acc as any)[settingsKey] = locked (acc as any)[settingsKey] = locked
? value ? value
: userDiverged : hasMeaningfulSettingValue(effectiveValue)
? userValue ? effectiveValue
: hasMeaningfulSettingValue(value) : value;
? value
: defaultValues[settingsKey];
} }
return acc; return acc;
}, {}); }, {});