Compare commits

..

6 Commits

Author SHA1 Message Date
Gauvino
def03c0e24 i18n(fr): complete & audit French translations
Rebased onto the i18n key cleanup (#1636): FR for the current 793-key set,
89 obsolete keys dropped, 9 newly-added source keys translated, plus the
Copilot fix (les reglages, lowercase after the article).
2026-06-01 12:17:00 +02:00
Gauvino
ac2bb104e4 refactor(i18n): harden key checker per review
- Validate static t() keys even under a dynamic prefix (dynamic prefixes now
  only affect the unused calc, not missing) — Copilot
- Strip JS/TS/JSX comments before scanning so keys in comments aren't counted
  as usage — CodeRabbit
- Run i18n:check as part of \un run test\ so local mirrors CI — CodeRabbit
- Fix stale 'all locale files' comment (fixer is source-only) — Copilot
2026-06-01 12:15:22 +02:00
Gauvino
5d3648e875 chore(i18n): propagate obsolete-key removal to all locales 2026-06-01 11:41:51 +02:00
Gauvino
8def6d3d46 chore(i18n): remove obsolete unused keys from en.json
Keeps 15 keys for planned features (watchlist add/remove, PIN confirm,
in-player subtitle search, stop-playback confirm) via the checker allow-list.
2026-06-01 11:41:49 +02:00
Gauvino
e8f21a879c fix(i18n): add 9 source keys referenced in code but missing from en.json 2026-06-01 11:41:47 +02:00
Gauvino
badbce34e1 ci(i18n): add unused & missing translation-key check to Quality Gate
Dependency-free Bun script (bun run i18n:check, added to the linting matrix):
flags keys referenced in code but missing from en.json, and keys in en.json
referenced nowhere. Handles dynamic t(\prefix.\\) and keys stored as string
constants (exact delimited-literal match). Source-only: only en.json is
checked/edited; Crowdin syncs the other locales. Planned-feature keys are
allow-listed in scripts/i18n-keys.config.json.
2026-06-01 11:41:44 +02:00
118 changed files with 1569 additions and 11605 deletions

View File

@@ -3,7 +3,7 @@
## Project Overview ## Project Overview
Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native).
It supports mobile (iOS/Android) and TV platforms, integrates with Jellyfin and Seerr APIs, It supports mobile (iOS/Android) and TV platforms, integrates with Jellyfin and Jellyseerr APIs,
and provides seamless media streaming with offline capabilities and Chromecast support. and provides seamless media streaming with offline capabilities and Chromecast support.
## Main Technologies ## Main Technologies
@@ -40,30 +40,9 @@ and provides seamless media streaming with offline capabilities and Chromecast s
- `scripts/` Automation scripts (Node.js, Bash) - `scripts/` Automation scripts (Node.js, Bash)
- `plugins/` Expo/Metro plugins - `plugins/` Expo/Metro plugins
## Code Quality Standards ## Coding Standards
**CRITICAL: Code must be production-ready, reliable, and maintainable**
### Type Safety
- Use TypeScript for ALL files (no .js files) - Use TypeScript for ALL files (no .js files)
- **NEVER use `any` type** - use proper types, generics, or `unknown` with type guards
- Use `@ts-expect-error` with detailed comments only when necessary (e.g., library limitations)
- When facing type issues, create proper type definitions and helper functions instead of using `any`
- Use type assertions (`as`) only as a last resort with clear documentation explaining why
- For Expo Router navigation: prefer string URLs with `URLSearchParams` over object syntax to avoid type conflicts
- Enable and respect strict TypeScript compiler options
- Define explicit return types for functions
- Use discriminated unions for complex state
### Code Reliability
- Implement comprehensive error handling with try-catch blocks
- Validate all external inputs (API responses, user input, query params)
- Handle edge cases explicitly (empty arrays, null, undefined)
- Use optional chaining (`?.`) and nullish coalescing (`??`) appropriately
- Add runtime checks for critical operations
- Implement proper loading and error states in components
### Best Practices
- Use descriptive English names for variables, functions, and components - Use descriptive English names for variables, functions, and components
- Prefer functional React components with hooks - Prefer functional React components with hooks
- Use Jotai atoms for global state management - Use Jotai atoms for global state management
@@ -71,10 +50,8 @@ and provides seamless media streaming with offline capabilities and Chromecast s
- Follow BiomeJS formatting and linting rules - Follow BiomeJS formatting and linting rules
- Use `const` over `let`, avoid `var` entirely - Use `const` over `let`, avoid `var` entirely
- Implement proper error boundaries - Implement proper error boundaries
- Use React.memo() for performance optimization when needed - Use React.memo() for performance optimization
- Handle both mobile and TV navigation patterns - Handle both mobile and TV navigation patterns
- Write self-documenting code with clear intent
- Add comments only when code complexity requires explanation
## API Integration ## API Integration
@@ -108,18 +85,6 @@ Exemples:
- `fix(auth): handle expired JWT tokens` - `fix(auth): handle expired JWT tokens`
- `chore(deps): update Jellyfin SDK` - `chore(deps): update Jellyfin SDK`
## Internationalization (i18n)
- **Primary workflow**: Always edit `translations/en.json` for new translation keys or updates
- **Translation files** (ar.json, ca.json, cs.json, de.json, etc.):
- **NEVER add or remove keys** - Crowdin manages the key structure
- **Editing translation values is safe** - Bidirectional sync handles merges
- Prefer letting Crowdin translators update values, but direct edits work if needed
- **Crowdin workflow**:
- New keys added to `en.json` sync to Crowdin automatically
- Approved translations sync back to language files via GitHub integration
- The source of truth is `en.json` for structure, Crowdin for translations
## Special Instructions ## Special Instructions
- Prioritize cross-platform compatibility (mobile + TV) - Prioritize cross-platform compatibility (mobile + TV)

View File

@@ -97,6 +97,7 @@ jobs:
- "check" - "check"
- "format" - "format"
- "typecheck" - "typecheck"
- "i18n:check"
steps: steps:
- name: "📥 Checkout PR code" - name: "📥 Checkout PR code"

View File

@@ -59,7 +59,6 @@ function SettingsMobile() {
<QuickConnect className='mb-4' /> <QuickConnect className='mb-4' />
{Platform.OS !== "ios" && (
<View className='mb-4'> <View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}> <ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem <ListItem
@@ -71,7 +70,6 @@ function SettingsMobile() {
/> />
</ListGroup> </ListGroup>
</View> </View>
)}
<View className='mb-4'> <View className='mb-4'>
<AppLanguageSelector /> <AppLanguageSelector />

View File

@@ -114,7 +114,7 @@ export default function StreamystatsPage() {
}; };
const handleRefreshFromServer = useCallback(async () => { const handleRefreshFromServer = useCallback(async () => {
const newPluginSettings = await refreshStreamyfinPluginSettings(); const newPluginSettings = await refreshStreamyfinPluginSettings(true);
// Update local state with new values // Update local state with new values
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || ""; const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
setUrl(newUrl); setUrl(newUrl);

View File

@@ -1,238 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "expo-router";
import { TFunction } from "i18next";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { PlatformDropdown } from "@/components/PlatformDropdown";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { useSettings } from "@/utils/atoms/settings";
/**
* Factory function to create skip options for a specific segment type
* Reduces code duplication across all 5 segment types
*/
const useSkipOptions = (
settingKey:
| "skipIntro"
| "skipOutro"
| "skipRecap"
| "skipCommercial"
| "skipPreview",
settings: ReturnType<typeof useSettings>["settings"] | null,
updateSettings: ReturnType<typeof useSettings>["updateSettings"],
t: TFunction<"translation", undefined>,
) => {
return useMemo(
() => [
{
options: SEGMENT_SKIP_OPTIONS(t).map((option) => ({
type: "radio" as const,
label: option.label,
value: option.value,
selected: option.value === settings?.[settingKey],
onPress: () => updateSettings({ [settingKey]: option.value }),
})),
},
],
[settings?.[settingKey], updateSettings, t, settingKey],
);
};
export default function SegmentSkipPage() {
const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation();
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
title: t("home.settings.other.segment_skip_settings"),
});
}, [navigation, t]);
const skipIntroOptions = useSkipOptions(
"skipIntro",
settings,
updateSettings,
t,
);
const skipOutroOptions = useSkipOptions(
"skipOutro",
settings,
updateSettings,
t,
);
const skipRecapOptions = useSkipOptions(
"skipRecap",
settings,
updateSettings,
t,
);
const skipCommercialOptions = useSkipOptions(
"skipCommercial",
settings,
updateSettings,
t,
);
const skipPreviewOptions = useSkipOptions(
"skipPreview",
settings,
updateSettings,
t,
);
if (!settings) return null;
return (
<DisabledSetting disabled={false} className='px-4'>
<ListGroup>
<ListItem
title={t("home.settings.other.skip_intro")}
subtitle={t("home.settings.other.skip_intro_description")}
disabled={pluginSettings?.skipIntro?.locked}
>
<PlatformDropdown
groups={skipIntroOptions}
disabled={pluginSettings?.skipIntro?.locked}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(`home.settings.other.segment_skip_${settings.skipIntro}`)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.skip_intro")}
/>
</ListItem>
<ListItem
title={t("home.settings.other.skip_outro")}
subtitle={t("home.settings.other.skip_outro_description")}
disabled={pluginSettings?.skipOutro?.locked}
>
<PlatformDropdown
groups={skipOutroOptions}
disabled={pluginSettings?.skipOutro?.locked}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(`home.settings.other.segment_skip_${settings.skipOutro}`)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.skip_outro")}
/>
</ListItem>
<ListItem
title={t("home.settings.other.skip_recap")}
subtitle={t("home.settings.other.skip_recap_description")}
disabled={pluginSettings?.skipRecap?.locked}
>
<PlatformDropdown
groups={skipRecapOptions}
disabled={pluginSettings?.skipRecap?.locked}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(`home.settings.other.segment_skip_${settings.skipRecap}`)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.skip_recap")}
/>
</ListItem>
<ListItem
title={t("home.settings.other.skip_commercial")}
subtitle={t("home.settings.other.skip_commercial_description")}
disabled={pluginSettings?.skipCommercial?.locked}
>
<PlatformDropdown
groups={skipCommercialOptions}
disabled={pluginSettings?.skipCommercial?.locked}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(
`home.settings.other.segment_skip_${settings.skipCommercial}`,
)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.skip_commercial")}
/>
</ListItem>
<ListItem
title={t("home.settings.other.skip_preview")}
subtitle={t("home.settings.other.skip_preview_description")}
disabled={pluginSettings?.skipPreview?.locked}
>
<PlatformDropdown
groups={skipPreviewOptions}
disabled={pluginSettings?.skipPreview?.locked}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(
`home.settings.other.segment_skip_${settings.skipPreview}`,
)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.skip_preview")}
/>
</ListItem>
</ListGroup>
</DisabledSetting>
);
}
const SEGMENT_SKIP_OPTIONS = (
t: TFunction<"translation", undefined>,
): Array<{
label: string;
value: "none" | "ask" | "auto";
}> => [
{
label: t("home.settings.other.segment_skip_auto"),
value: "auto",
},
{
label: t("home.settings.other.segment_skip_ask"),
value: "ask",
},
{
label: t("home.settings.other.segment_skip_none"),
value: "none",
},
];

View File

@@ -166,7 +166,7 @@ export default function IndexLayout() {
open={dropdownOpen} open={dropdownOpen}
onOpenChange={setDropdownOpen} onOpenChange={setDropdownOpen}
trigger={ trigger={
<View> <View className='pl-1.5'>
<Ionicons <Ionicons
name='ellipsis-horizontal-outline' name='ellipsis-horizontal-outline'
size={24} size={24}

View File

@@ -11,8 +11,6 @@ import type {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
import { CastAutoplayWatcher } from "@/components/casting/CastAutoplayWatcher";
import { CastingMiniPlayer } from "@/components/casting/CastingMiniPlayer";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useTVHomeBackHandler } from "@/hooks/useTVBackHandler"; import { useTVHomeBackHandler } from "@/hooks/useTVBackHandler";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -141,8 +139,6 @@ export default function TabLayout() {
}} }}
/> />
</NativeTabs> </NativeTabs>
<CastingMiniPlayer />
<CastAutoplayWatcher />
<MiniPlayerBar /> <MiniPlayerBar />
<MusicPlaybackEngine /> <MusicPlaybackEngine />
</View> </View>

View File

@@ -1,768 +0,0 @@
/**
* Unified Casting Player Modal
* Protocol-agnostic full-screen player for all supported casting protocols
*/
import { router, Stack } from "expo-router";
import { useAtomValue, useSetAtom } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { GestureDetector } from "react-native-gesture-handler";
import GoogleCast, {
CastState,
MediaPlayerState,
useCastDevice,
useCastState,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import Animated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BITRATES } from "@/components/BitrateSelector";
import { CastPlayerEpisodeControls } from "@/components/casting/player/CastPlayerEpisodeControls";
import { CastPlayerHeader } from "@/components/casting/player/CastPlayerHeader";
import { CastPlayerPoster } from "@/components/casting/player/CastPlayerPoster";
import { CastPlayerProgressBar } from "@/components/casting/player/CastPlayerProgressBar";
import { CastPlayerTitle } from "@/components/casting/player/CastPlayerTitle";
import { CastPlayerTransportControls } from "@/components/casting/player/CastPlayerTransportControls";
import { ChapterList } from "@/components/chapters/ChapterList";
import { ChromecastDeviceSheet } from "@/components/chromecast/ChromecastDeviceSheet";
import { ChromecastEpisodeList } from "@/components/chromecast/ChromecastEpisodeList";
import { ChromecastSettingsMenu } from "@/components/chromecast/ChromecastSettingsMenu";
import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
import { Text } from "@/components/common/Text";
import { AutoplayCountdown } from "@/components/player/AutoplayCountdown";
import { useCastDismissGesture } from "@/hooks/useCastDismissGesture";
import { useCastEpisodes } from "@/hooks/useCastEpisodes";
import { useCasting } from "@/hooks/useCasting";
import { useCastPlayerItem } from "@/hooks/useCastPlayerItem";
import { useCastPlayerProgress } from "@/hooks/useCastPlayerProgress";
import { useCastSelection } from "@/hooks/useCastSelection";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { castAutoplayAtom } from "@/utils/atoms/castAutoplay";
import { useSettings } from "@/utils/atoms/settings";
import { detectCapabilities } from "@/utils/casting/capabilities";
import { loadCastMedia } from "@/utils/casting/castLoad";
import { getPosterUrl } from "@/utils/casting/helpers";
import { resolveSelection } from "@/utils/casting/selection";
import type { CastSelection } from "@/utils/casting/types";
import { chapterMarkers } from "@/utils/chapters";
import {
type PlaybackController,
useRegisterPlaybackController,
} from "@/utils/playback/playbackController";
export default function CastingPlayerScreen() {
const insets = useSafeAreaInsets();
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const { settings, updateSettings } = useSettings();
const { t } = useTranslation();
// Chromecast autoplay countdown — watcher hook drives this atom; we render
// the overlay here when set, and handle Play-now / Cancel from the user.
const castAutoplay = useAtomValue(castAutoplayAtom);
const setCastAutoplay = useSetAtom(castAutoplayAtom);
// Get raw Chromecast state directly - same as old implementation
const castState = useCastState();
const mediaStatus = useMediaStatus();
const castDevice = useCastDevice();
// Keep hook active for connection - used by remoteMediaClient from useCasting
useRemoteMediaClient();
// Fetch full item data from Jellyfin by ID and derive the effective item
const { fetchedItem, currentItem } = useCastPlayerItem({
api,
user,
mediaStatus,
});
// Derive state from raw Chromecast hooks
const duration = mediaStatus?.mediaInfo?.streamDuration ?? 0;
const isPlaying = mediaStatus?.playerState === MediaPlayerState.PLAYING;
const isBuffering = mediaStatus?.playerState === MediaPlayerState.BUFFERING;
const currentDevice = castDevice?.friendlyName ?? null;
// Progress/slider/trickplay cluster: slider shared values, scrub state,
// live-progress interpolation, resume-position tracking, trickplay preview.
const {
sliderProgress,
sliderMin,
sliderMax,
isScrubbing,
trickplayTime,
setTrickplayTime,
progress,
resumePositionRef,
trickPlayUrl,
calculateTrickplayUrl,
trickplayInfo,
} = useCastPlayerProgress({ mediaStatus, fetchedItem, duration });
// Only use casting controls if we have a current item to avoid "No session" errors
const castingControls = useCasting(currentItem);
const {
togglePlayPause,
skipForward,
skipBackward,
setVolume,
volume,
remoteMediaClient,
} = currentItem
? castingControls
: {
togglePlayPause: async () => {},
skipForward: async () => {},
skipBackward: async () => {},
setVolume: () => {},
volume: 1,
remoteMediaClient: null,
};
// Modal states
const [showEpisodeList, setShowEpisodeList] = useState(false);
const [showDeviceSheet, setShowDeviceSheet] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [chapterListVisible, setChapterListVisible] = useState(false);
// Chapter markers (shown for both episodes and movies).
const chapters = currentItem?.Chapters;
const hasChapters = chapterMarkers(chapters, duration * 1000).length > 1;
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1);
// Reload the cast stream with a full selection; resolves true on success.
const reloadWithSelection = useCallback(
async (selection: CastSelection): Promise<boolean> => {
if (!api || !user?.Id || !currentItem?.Id || !remoteMediaClient) {
console.warn("[Casting Player] Cannot reload - missing required data");
return false;
}
const currentPosition = resumePositionRef.current;
const result = await loadCastMedia({
client: remoteMediaClient,
device: castDevice,
api,
item: currentItem,
userId: user.Id,
profileMode: settings.chromecastProfile,
maxBitrateSetting: settings.chromecastMaxBitrate,
options: {
mediaSourceId: selection.mediaSourceId,
audioStreamIndex: selection.audioStreamIndex,
subtitleStreamIndex: selection.subtitleStreamIndex,
maxBitrate: selection.maxBitrate,
startPositionMs: currentPosition * 1000,
},
});
if (!result.ok) {
console.error(
"[Casting Player] Failed to reload stream:",
result.error,
);
return false;
}
return true;
},
[
api,
user?.Id,
currentItem,
remoteMediaClient,
castDevice,
settings.chromecastProfile,
settings.chromecastMaxBitrate,
],
);
const { currentSelection, applySelection } = useCastSelection({
currentItem,
mediaStatus,
reload: reloadWithSelection,
});
// Episode/season cluster: episode list, next episode, season data, loader
const { episodes, nextEpisode, seasonData, loadEpisode, loadingEpisodeId } =
useCastEpisodes({
api,
user,
currentItem,
remoteMediaClient,
castDevice,
settings,
});
// True while a `loadEpisode` is in flight and `currentItem` (derived from the
// cast customData) still describes the previous episode. Used to suppress
// episode-dependent secondary UI that would otherwise flash stale data.
const isEpisodeTransitioning =
loadingEpisodeId != null && loadingEpisodeId !== currentItem?.Id;
// Expose this player to the app-wide remote-control surface while a cast
// session is connected. The individual useCasting methods are each
// useCallback-wrapped and stable, so depend on them directly rather than on
// the whole `castingControls` object literal (rebuilt every render).
const {
togglePlayPause: castTogglePlayPause,
pause: castPause,
play: castPlay,
stop: castStop,
seek: castSeek,
setVolume: castSetVolume,
} = castingControls;
// toggleMute reads the latest volume without making `volume` a useMemo dep.
const volumeRef = useRef(volume);
volumeRef.current = volume;
const castController = useMemo<PlaybackController>(
() => ({
playPause: () => {
castTogglePlayPause();
},
pause: () => {
castPause();
},
unpause: () => {
castPlay();
},
stop: () => {
castStop();
},
seek: (positionMs) => {
castSeek(positionMs);
},
next: () => {
if (nextEpisode) loadEpisode(nextEpisode);
},
previous: () => {
const idx = episodes.findIndex((e) => e.Id === currentItem?.Id);
if (idx > 0) loadEpisode(episodes[idx - 1]);
},
setVolume: (level) => {
castSetVolume(level);
},
toggleMute: () => {
castSetVolume(volumeRef.current > 0 ? 0 : 1);
},
}),
[
castTogglePlayPause,
castPause,
castPlay,
castStop,
castSeek,
castSetVolume,
episodes,
nextEpisode,
loadEpisode,
currentItem?.Id,
],
);
useRegisterPlaybackController(
castController,
castState === CastState.CONNECTED,
);
// The MediaSource currently selected, for deriving its tracks.
// Derived from fetchedItem: the slim cast-customData item strips per-source
// MediaStreams, so only the full fetched item yields correct track lists.
const selectedSource = useMemo(
() =>
fetchedItem?.MediaSources?.find(
(s) => s.Id === currentSelection?.mediaSourceId,
) ??
fetchedItem?.MediaSources?.[0] ??
null,
[fetchedItem?.MediaSources, currentSelection?.mediaSourceId],
);
// Real alternate versions (multi-version items).
const availableVersions = useMemo(
() =>
(fetchedItem?.MediaSources ?? []).map((s, i) => ({
id: s.Id ?? `source-${i}`,
name: s.Name || `${t("casting_player.version")} ${i + 1}`,
})),
[fetchedItem?.MediaSources, t],
);
// Quality tiers from the shared ladder, capped to BOTH the device's
// capability and the media's own bitrate — a tier above either ceiling
// would behave identically to "Max", so it is not offered.
const availableQualities = useMemo(() => {
const caps = detectCapabilities(castDevice, {
profileMode: settings.chromecastProfile,
maxBitrate: settings.chromecastMaxBitrate,
});
const mediaBitrate =
selectedSource?.Bitrate ??
fetchedItem?.MediaStreams?.find((s) => s.Type === "Video")?.BitRate ??
Number.POSITIVE_INFINITY;
const ceiling = Math.min(caps.maxVideoBitrate, mediaBitrate);
return BITRATES.filter((b) => b.value === undefined || b.value <= ceiling);
}, [
castDevice,
settings.chromecastProfile,
settings.chromecastMaxBitrate,
selectedSource,
fetchedItem?.MediaStreams,
]);
const availableAudioTracks = useMemo(() => {
const streams = selectedSource?.MediaStreams ?? fetchedItem?.MediaStreams;
if (!streams) return [];
return streams
.filter((stream) => stream.Type === "Audio")
.map((stream) => ({
index: stream.Index ?? 0,
language: stream.Language || "Unknown",
displayTitle:
stream.DisplayTitle ||
`${stream.Language || "Unknown"} ${stream.Codec || ""}`.trim(),
codec: stream.Codec || "Unknown",
channels: stream.Channels,
bitrate: stream.BitRate,
}));
}, [selectedSource, fetchedItem?.MediaStreams]);
const availableSubtitleTracks = useMemo(() => {
const streams = selectedSource?.MediaStreams ?? fetchedItem?.MediaStreams;
if (!streams) return [];
return streams
.filter((stream) => stream.Type === "Subtitle")
.map((stream) => ({
index: stream.Index ?? 0,
language: stream.Language || "Unknown",
displayTitle:
stream.DisplayTitle ||
[
stream.Language || "Unknown",
stream.IsForced ? " (Forced)" : "",
stream.Title ? ` - ${stream.Title}` : "",
].join(""),
codec: stream.Codec || "Unknown",
isForced: stream.IsForced || false,
isExternal: stream.IsExternal || false,
}));
}, [selectedSource, fetchedItem?.MediaStreams]);
// Autoplay overlay's "Play now" — load the queued next episode immediately.
// Mirrors `useCastEpisodes.loadEpisode` exactly (same `loadCastMedia` shape,
// same start-position derivation) so the cast load is identical regardless
// of whether it is triggered by the user or by the countdown timer.
const onAutoplayPlayNow = useCallback(async () => {
if (!castAutoplay) return;
const episode = castAutoplay.nextEpisode;
if (!api || !user?.Id || !remoteMediaClient || !episode?.Id) {
setCastAutoplay(null);
return;
}
try {
const startPositionMs =
(episode.UserData?.PlaybackPositionTicks ?? 0) / 10000;
const result = await loadCastMedia({
client: remoteMediaClient,
device: castDevice,
api,
item: episode,
userId: user.Id,
profileMode: settings.chromecastProfile,
maxBitrateSetting: settings.chromecastMaxBitrate,
options: { startPositionMs },
});
if (!result.ok) {
console.error(
"[Casting Player] Failed to load next episode (play now):",
result.error,
);
return;
}
// Reset the autoplay counter on explicit user action.
updateSettings({ autoPlayEpisodeCount: 0 });
} catch (error) {
console.error(
"[Casting Player] Failed to load next episode (play now):",
error,
);
} finally {
setCastAutoplay(null);
}
}, [
castAutoplay,
api,
user?.Id,
remoteMediaClient,
castDevice,
settings.chromecastProfile,
settings.chromecastMaxBitrate,
updateSettings,
setCastAutoplay,
]);
// Poster URL for the queued next episode (mirrors `posterUrl` for the
// currently-playing item — same helper, same dimensions).
const autoplayPosterUrl = useMemo(() => {
if (!castAutoplay || !api?.basePath) return null;
const ep = castAutoplay.nextEpisode;
// `BaseItemDto.Id` is `string | undefined`; bail if missing so we never
// call the helper with `undefined`. AutoplayCountdown handles null.
if (!ep?.Id) return null;
return getPosterUrl(api.basePath, ep.Id, ep.ImageTags?.Primary, 260, 390);
}, [castAutoplay, api?.basePath]);
// NOTE: Auto-navigation to casting-player is handled by higher-level
// components (e.g., CastingMiniPlayer or Chromecast button). We intentionally
// do NOT call router.replace("/casting-player") here because this component
// IS the casting-player screen — doing so would cause redundant navigation loops.
// Segment detection (skip intro/credits) - use progress in seconds for accurate detection
const { currentSegment, skipIntro, skipCredits, skipSegment } =
useChromecastSegments(currentItem, progress * 1000, false);
// Swipe down to dismiss gesture
const { panGesture, animatedStyle, dismissModal } = useCastDismissGesture({
router,
});
// Memoize expensive calculations (before early return)
const posterUrl = useMemo(() => {
if (!api?.basePath || !currentItem?.Id) return null;
// For episodes, use SEASON poster instead of episode poster
if (currentItem.Type === "Episode" && seasonData?.Id) {
// Use ParentPrimaryImageItemId if available, otherwise use season's own ImageTags
const imageItemId = seasonData.ParentPrimaryImageItemId || seasonData.Id;
const seasonImageTag = seasonData.ImageTags?.Primary;
return seasonImageTag
? `${api.basePath}/Items/${imageItemId}/Images/Primary?fillHeight=450&fillWidth=300&quality=96&tag=${seasonImageTag}`
: `${api.basePath}/Items/${imageItemId}/Images/Primary?fillHeight=450&fillWidth=300&quality=96`;
}
// Fallback to item poster for non-episodes or if season data not loaded
return getPosterUrl(
api.basePath,
currentItem.Id,
currentItem.ImageTags?.Primary,
260,
390,
);
}, [
api?.basePath,
currentItem?.Id,
currentItem?.Type,
seasonData?.Id,
seasonData?.ImageTags?.Primary,
currentItem?.ImageTags?.Primary,
]);
const protocolColor = "#a855f7"; // Streamyfin purple
// Redirect if not connected - check CastState like old implementation
useEffect(() => {
// Redirect immediately when disconnected or no devices
if (
castState === CastState.NOT_CONNECTED ||
castState === CastState.NO_DEVICES_AVAILABLE
) {
// Use setTimeout to avoid state update during render
const timer = setTimeout(() => {
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
}, 100);
return () => clearTimeout(timer);
}
}, [castState, router]);
// Also redirect if mediaStatus disappears (media ended or stopped)
useEffect(() => {
if (castState === CastState.CONNECTED && !mediaStatus) {
const timer = setTimeout(() => {
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
}, 500); // Small delay to allow for media transitions
return () => clearTimeout(timer);
}
}, [castState, mediaStatus, router]);
// Show loading while connecting
if (castState === CastState.CONNECTING) {
return (
<View
style={{
flex: 1,
backgroundColor: "#000",
alignItems: "center",
justifyContent: "center",
}}
>
<ActivityIndicator size='large' color='#fff' />
<Text style={{ color: "#fff", marginTop: 16 }}>
{t("casting_player.connecting")}
</Text>
</View>
);
}
// Don't render if not connected or no media playing
if (castState !== CastState.CONNECTED || !mediaStatus || !currentItem) {
return null;
}
return (
<>
<Stack.Screen
options={{
headerShown: false,
title: "",
presentation: "fullScreenModal",
animation: "slide_from_bottom",
}}
/>
<GestureDetector gesture={panGesture}>
<Animated.View
style={[
{
flex: 1,
backgroundColor: "#000",
},
animatedStyle,
]}
>
{/* Header - Fixed at top */}
<CastPlayerHeader
insetTop={insets.top}
protocolColor={protocolColor}
currentDevice={currentDevice}
t={t}
onDismiss={dismissModal}
onPressConnectionIndicator={() => setShowDeviceSheet(true)}
onPressSettings={() => setShowSettings(true)}
/>
{/* Title Area — hidden during an episode change to avoid flashing
the previous episode's title/season-episode numbers. */}
{!isEpisodeTransitioning && (
<CastPlayerTitle
insetTop={insets.top}
currentItem={currentItem}
t={t}
/>
)}
{/* Scrollable content area */}
<ScrollView
contentContainerStyle={{
paddingHorizontal: 20,
paddingTop: insets.top + 160,
paddingBottom: insets.bottom + 500,
}}
showsVerticalScrollIndicator={false}
>
{/* Poster with buffering overlay — force the overlay during an
episode change so the loading state covers the stale poster. */}
<CastPlayerPoster
posterUrl={posterUrl}
isBuffering={isBuffering || isEpisodeTransitioning}
currentSegment={currentSegment}
skipIntro={skipIntro}
skipCredits={skipCredits}
skipSegment={skipSegment}
remoteMediaClient={remoteMediaClient}
mediaStatus={mediaStatus}
protocolColor={protocolColor}
t={t}
/>
</ScrollView>
{/* Fixed control row - positioned independently. Episode-specific
buttons are conditional inside; Stop is always available. */}
<CastPlayerEpisodeControls
insetBottom={insets.bottom}
currentItemId={currentItem.Id}
episodes={episodes}
nextEpisode={nextEpisode}
remoteMediaClient={remoteMediaClient}
onPressEpisodes={() => setShowEpisodeList(true)}
hasChapters={hasChapters}
onPressChapters={() => setChapterListVisible(true)}
loadEpisode={loadEpisode}
router={router}
/>
{/* Fixed bottom controls area */}
<View
style={{
position: "absolute",
bottom: insets.bottom + 10,
left: 20,
right: 20,
zIndex: 98,
}}
>
{/* Progress slider with trickplay preview + time display */}
<CastPlayerProgressBar
sliderProgress={sliderProgress}
sliderMin={sliderMin}
sliderMax={sliderMax}
isScrubbing={isScrubbing}
trickplayTime={trickplayTime}
setTrickplayTime={setTrickplayTime}
trickPlayUrl={trickPlayUrl}
calculateTrickplayUrl={calculateTrickplayUrl}
trickplayInfo={trickplayInfo}
progress={progress}
duration={duration}
remoteMediaClient={remoteMediaClient}
protocolColor={protocolColor}
chapters={currentItem?.Chapters}
t={t}
/>
{/* Playback controls */}
<CastPlayerTransportControls
isPlaying={isPlaying}
togglePlayPause={togglePlayPause}
skipBackward={skipBackward}
skipForward={skipForward}
rewindSkipTime={settings?.rewindSkipTime}
forwardSkipTime={settings?.forwardSkipTime}
protocolColor={protocolColor}
/>
</View>
{/* Autoplay countdown overlay — bottom-centred above the episode
control row and main controls. 320 wide card; centred via
left/right:0 + alignItems:"center". */}
{castAutoplay && (
<View
style={{
position: "absolute",
bottom: insets.bottom + 280,
left: 0,
right: 0,
alignItems: "center",
zIndex: 99,
}}
pointerEvents='box-none'
>
<AutoplayCountdown
nextEpisode={castAutoplay.nextEpisode}
posterUrl={autoplayPosterUrl}
secondsRemaining={castAutoplay.secondsRemaining}
onPlayNow={onAutoplayPlayNow}
onCancel={() => setCastAutoplay(null)}
/>
</View>
)}
{/* Modals */}
<ChromecastDeviceSheet
visible={showDeviceSheet}
onClose={() => setShowDeviceSheet(false)}
device={
currentDevice && castDevice
? { friendlyName: currentDevice }
: null
}
onDisconnect={async () => {
try {
// End the casting session and disconnect completely
const sessionManager = GoogleCast.getSessionManager();
await sessionManager.endCurrentSession(true);
setShowDeviceSheet(false);
// Close player immediately after disconnecting
setTimeout(() => {
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
}, 100);
} catch (error) {
console.error(
"[Casting Player] Error disconnecting from Chromecast:",
error,
);
}
}}
volume={volume}
onVolumeChange={async (vol) => {
try {
setVolume(vol);
} catch (error) {
console.error("[Casting Player] Failed to set volume:", error);
}
}}
/>
<ChromecastEpisodeList
visible={showEpisodeList}
onClose={() => setShowEpisodeList(false)}
currentItem={currentItem}
episodes={episodes}
api={api}
onSelectEpisode={async (episode) => {
setShowEpisodeList(false);
await loadEpisode(episode);
}}
/>
<ChapterList
visible={chapterListVisible}
chapters={chapters}
currentPositionMs={progress * 1000}
onSeek={(ms) => {
remoteMediaClient?.seek({ position: ms / 1000 });
}}
onClose={() => setChapterListVisible(false)}
/>
<ChromecastSettingsMenu
visible={showSettings}
onClose={() => setShowSettings(false)}
versions={availableVersions}
selectedVersionId={currentSelection?.mediaSourceId ?? ""}
onVersionChange={(id) => {
if (!fetchedItem) return;
applySelection({
...resolveSelection(fetchedItem, { mediaSourceId: id }),
maxBitrate: currentSelection?.maxBitrate,
});
}}
qualities={availableQualities}
selectedMaxBitrate={currentSelection?.maxBitrate}
onQualityChange={(value) => applySelection({ maxBitrate: value })}
audioTracks={isEpisodeTransitioning ? [] : availableAudioTracks}
selectedAudioIndex={currentSelection?.audioStreamIndex ?? -1}
onAudioChange={(index) =>
applySelection({ audioStreamIndex: index })
}
subtitleTracks={
isEpisodeTransitioning ? [] : availableSubtitleTracks
}
selectedSubtitleIndex={currentSelection?.subtitleStreamIndex ?? -1}
onSubtitleChange={(index) =>
applySelection({ subtitleStreamIndex: index })
}
playbackSpeed={currentPlaybackSpeed}
onPlaybackSpeedChange={(speed) => {
setCurrentPlaybackSpeed(speed);
remoteMediaClient?.setPlaybackRate(speed).catch(console.error);
}}
/>
</Animated.View>
</GestureDetector>
</>
);
}

View File

@@ -49,6 +49,7 @@ import { DownloadedItem } from "@/providers/Downloads/types";
import { useInactivity } from "@/providers/InactivityProvider"; import { useInactivity } from "@/providers/InactivityProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider"; import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles"; import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
@@ -59,10 +60,6 @@ import {
getMpvSubtitleId, getMpvSubtitleId,
} from "@/utils/jellyfin/subtitleUtils"; } from "@/utils/jellyfin/subtitleUtils";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import {
type PlaybackController,
useRegisterPlaybackController,
} from "@/utils/playback/playbackController";
import { msToTicks, ticksToSeconds } from "@/utils/time"; import { msToTicks, ticksToSeconds } from "@/utils/time";
import { generateDeviceProfile } from "../../../utils/profiles/native"; import { generateDeviceProfile } from "../../../utils/profiles/native";
@@ -406,6 +403,26 @@ export default function DirectPlayerPage() {
reportPlaybackStart(); reportPlaybackStart();
}, [stream, api, offline]); }, [stream, api, offline]);
const togglePlay = async () => {
lightHapticFeedback();
setIsPlaying(!isPlaying);
if (isPlaying) {
await videoRef.current?.pause();
const progressInfo = currentPlayStateInfo();
if (progressInfo) {
playbackManager.reportPlaybackProgress(progressInfo);
}
} else {
videoRef.current?.play();
const progressInfo = currentPlayStateInfo();
if (!offline && api) {
await getPlaystateApi(api).reportPlaybackStart({
playbackStartInfo: progressInfo,
});
}
}
};
const reportPlaybackStopped = useCallback(async () => { const reportPlaybackStopped = useCallback(async () => {
if (!item?.Id || !stream?.sessionId || offline || !api) return; if (!item?.Id || !stream?.sessionId || offline || !api) return;
@@ -479,35 +496,6 @@ export default function DirectPlayerPage() {
isMuted, isMuted,
]); ]);
// Declared after currentPlayStateInfo so the dependency array can reference
// it without hitting the temporal dead zone.
const togglePlay = useCallback(async () => {
lightHapticFeedback();
setIsPlaying(!isPlaying);
if (isPlaying) {
await videoRef.current?.pause();
const progressInfo = currentPlayStateInfo();
if (progressInfo) {
playbackManager.reportPlaybackProgress(progressInfo);
}
} else {
videoRef.current?.play();
const progressInfo = currentPlayStateInfo();
if (!offline && api) {
await getPlaystateApi(api).reportPlaybackStart({
playbackStartInfo: progressInfo,
});
}
}
}, [
lightHapticFeedback,
isPlaying,
currentPlayStateInfo,
playbackManager,
offline,
api,
]);
const lastUrlUpdateTime = useSharedValue(0); const lastUrlUpdateTime = useSharedValue(0);
const wasJustSeeking = useSharedValue(false); const wasJustSeeking = useSharedValue(false);
const URL_UPDATE_INTERVAL = 30000; // Update URL every 30 seconds instead of every second const URL_UPDATE_INTERVAL = 30000; // Update URL every 30 seconds instead of every second
@@ -936,47 +924,6 @@ export default function DirectPlayerPage() {
return (await videoRef.current?.getTechnicalInfo?.()) ?? {}; return (await videoRef.current?.getTechnicalInfo?.()) ?? {};
}, []); }, []);
// App-wide remote control: wrap the player's existing handlers so remote
// commands (e.g. dashboard, WebSocket) route to whatever is playing.
const playbackController = useMemo<PlaybackController>(
() => ({
// togglePlay flips play/pause and reports progress to the server.
playPause: () => {
void togglePlay();
},
pause: () => {
pause();
},
unpause: () => {
play();
},
stop: () => {
stop();
},
// PlaybackController seeks in ms; the player's seek already expects ms.
seek: (positionMs: number) => {
seek(positionMs);
},
// The player screen has no episode-loading path of its own — episode
// navigation is handled inside <Controls> via router replacement — so
// next/previous are honest no-ops here.
next: () => {},
previous: () => {},
// Volume is device-level (react-native-volume-manager); the slider sends
// 0-1 while setVolumeCb expects 0-100.
setVolume: (level: number) => {
void setVolumeCb(level * 100);
},
toggleMute: () => {
void toggleMuteCb();
},
}),
[togglePlay, pause, play, stop, seek, setVolumeCb, toggleMuteCb],
);
// Active for the whole lifetime of the player screen; cleared on unmount.
useRegisterPlaybackController(playbackController, true);
// Determine play method based on stream URL and media source // Determine play method based on stream URL and media source
const playMethod = useMemo< const playMethod = useMemo<
"DirectPlay" | "DirectStream" | "Transcode" | undefined "DirectPlay" | "DirectStream" | "Transcode" | undefined
@@ -1313,7 +1260,7 @@ export default function DirectPlayerPage() {
console.error("Video Error:", e.nativeEvent); console.error("Video Error:", e.nativeEvent);
Alert.alert( Alert.alert(
t("player.error"), t("player.error"),
t("player.an_error_occurred_while_playing_the_video"), t("player.an_error_occured_while_playing_the_video"),
); );
writeToLog("ERROR", "Video Error", e.nativeEvent); writeToLog("ERROR", "Video Error", e.nativeEvent);
}} }}

122
components/BitRateSheet.tsx Normal file
View File

@@ -0,0 +1,122 @@
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";
export type Bitrate = {
key: string;
value: number | undefined;
};
export const BITRATES: Bitrate[] = [
{
key: "Max",
value: undefined,
},
{
key: "8 Mb/s",
value: 8000000,
height: 1080,
},
{
key: "4 Mb/s",
value: 4000000,
height: 1080,
},
{
key: "2 Mb/s",
value: 2000000,
},
{
key: "1 Mb/s",
value: 1000000,
},
{
key: "500 Kb/s",
value: 500000,
},
{
key: "250 Kb/s",
value: 250000,
},
].sort(
(a, b) =>
(b.value || Number.POSITIVE_INFINITY) -
(a.value || Number.POSITIVE_INFINITY),
);
interface Props extends React.ComponentProps<typeof View> {
onChange: (value: Bitrate) => void;
selected?: Bitrate | null;
inverted?: boolean | null;
}
export const BitrateSheet: React.FC<Props> = ({
onChange,
selected,
inverted,
...props
}) => {
const isTv = Platform.isTV;
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const sorted = useMemo(() => {
if (inverted)
return BITRATES.slice().sort(
(a, b) =>
(a.value || Number.POSITIVE_INFINITY) -
(b.value || Number.POSITIVE_INFINITY),
);
return BITRATES.slice().sort(
(a, b) =>
(b.value || Number.POSITIVE_INFINITY) -
(a.value || Number.POSITIVE_INFINITY),
);
}, [inverted]);
if (isTv) return null;
return (
<View
className='flex shrink'
style={{
minWidth: 60,
maxWidth: 200,
}}
>
<View className='flex flex-col' {...props}>
<Text className='opacity-50 mb-1 text-xs'>
{t("item_card.quality")}
</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}>
{BITRATES.find((b) => b.value === selected?.value)?.key}
</Text>
</TouchableOpacity>
</View>
<FilterSheet
open={open}
setOpen={setOpen}
title={t("item_card.quality")}
data={sorted}
values={selected ? [selected] : []}
multiple={false}
searchFilter={(item, query) => {
const label = (item as any).key || "";
return label.toLowerCase().includes(query.toLowerCase());
}}
renderItemLabel={(item) => <Text>{(item as any).key || ""}</Text>}
set={(vals) => {
const chosen = vals[0] as Bitrate | undefined;
if (chosen) onChange(chosen);
}}
/>
</View>
);
};

View File

@@ -10,31 +10,36 @@ export type Bitrate = {
}; };
export const BITRATES: Bitrate[] = [ export const BITRATES: Bitrate[] = [
{ key: "Max", value: undefined }, {
{ key: "200 Mb/s", value: 200000000 }, key: "Max",
{ key: "180 Mb/s", value: 180000000 }, value: undefined,
{ key: "140 Mb/s", value: 140000000 }, },
{ key: "120 Mb/s", value: 120000000 }, {
{ key: "110 Mb/s", value: 110000000 }, key: "8 Mb/s",
{ key: "100 Mb/s", value: 100000000 }, value: 8000000,
{ key: "90 Mb/s", value: 90000000 }, height: 1080,
{ key: "80 Mb/s", value: 80000000 }, },
{ key: "70 Mb/s", value: 70000000 }, {
{ key: "60 Mb/s", value: 60000000 }, key: "4 Mb/s",
{ key: "50 Mb/s", value: 50000000 }, value: 4000000,
{ key: "40 Mb/s", value: 40000000 }, height: 1080,
{ key: "30 Mb/s", value: 30000000 }, },
{ key: "20 Mb/s", value: 20000000 }, {
{ key: "15 Mb/s", value: 15000000 }, key: "2 Mb/s",
{ key: "10 Mb/s", value: 10000000 }, value: 2000000,
{ key: "8 Mb/s", value: 8000000 }, },
{ key: "5 Mb/s", value: 5000000 }, {
{ key: "4 Mb/s", value: 4000000 }, key: "1 Mb/s",
{ key: "3 Mb/s", value: 3000000 }, value: 1000000,
{ key: "2 Mb/s", value: 2000000 }, },
{ key: "1 Mb/s", value: 1000000 }, {
{ key: "720 Kb/s", value: 720000 }, key: "500 Kb/s",
{ key: "420 Kb/s", value: 420000 }, value: 500000,
},
{
key: "250 Kb/s",
value: 250000,
},
].sort( ].sort(
(a, b) => (a, b) =>
(b.value || Number.POSITIVE_INFINITY) - (b.value || Number.POSITIVE_INFINITY) -

View File

@@ -1,23 +1,15 @@
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import type { PlaybackProgressInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { useCallback, useEffect } from "react";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { router } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useRef, useState } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { Pressable } from "react-native-gesture-handler"; import { Pressable } from "react-native-gesture-handler";
import GoogleCast, { import GoogleCast, {
CastButton, CastButton,
CastContext, CastContext,
CastState,
useCastDevice, useCastDevice,
useCastState,
useDevices, useDevices,
useMediaStatus, useMediaStatus,
useRemoteMediaClient, useRemoteMediaClient,
} from "react-native-google-cast"; } from "react-native-google-cast";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { ChromecastConnectionMenu } from "./chromecast/ChromecastConnectionMenu";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
export function Chromecast({ export function Chromecast({
@@ -26,136 +18,23 @@ export function Chromecast({
background = "transparent", background = "transparent",
...props ...props
}) { }) {
// Hooks called for their side effects (keep Chromecast session active) const client = useRemoteMediaClient();
useRemoteMediaClient(); const castDevice = useCastDevice();
useCastDevice(); const devices = useDevices();
const castState = useCastState(); const sessionManager = GoogleCast.getSessionManager();
useDevices();
const discoveryManager = GoogleCast.getDiscoveryManager(); const discoveryManager = GoogleCast.getDiscoveryManager();
const mediaStatus = useMediaStatus(); const mediaStatus = useMediaStatus();
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
// Connection menu state
const [showConnectionMenu, setShowConnectionMenu] = useState(false);
const isConnected = castState === CastState.CONNECTED;
const lastReportedProgressRef = useRef(0);
const lastReportedPlayerStateRef = useRef<string | null>(null);
const playSessionIdRef = useRef<string | null>(null);
const lastContentIdRef = useRef<string | null>(null);
const discoveryAttempts = useRef(0);
const maxDiscoveryAttempts = 3;
// Enhanced discovery with retry mechanism - runs once on mount
useEffect(() => { useEffect(() => {
let isSubscribed = true; (async () => {
let retryTimeout: NodeJS.Timeout;
const startDiscoveryWithRetry = async () => {
if (!discoveryManager) { if (!discoveryManager) {
console.warn("DiscoveryManager is not initialized");
return; return;
} }
try {
// Stop any existing discovery first
try {
await discoveryManager.stopDiscovery();
} catch {
// Ignore errors when stopping
}
// Start fresh discovery
await discoveryManager.startDiscovery(); await discoveryManager.startDiscovery();
discoveryAttempts.current = 0; // Reset on success })();
} catch (error) { }, [client, devices, castDevice, sessionManager, discoveryManager]);
console.error("[Chromecast Discovery] Failed:", error);
// Retry on error
if (discoveryAttempts.current < maxDiscoveryAttempts && isSubscribed) {
discoveryAttempts.current++;
retryTimeout = setTimeout(() => {
if (isSubscribed) {
startDiscoveryWithRetry();
}
}, 2000);
}
}
};
startDiscoveryWithRetry();
return () => {
isSubscribed = false;
if (retryTimeout) {
clearTimeout(retryTimeout);
}
};
}, [discoveryManager]); // Only re-run if discoveryManager changes
// Report video progress to Jellyfin server
useEffect(() => {
if (!api || !user?.Id || !mediaStatus?.mediaInfo?.contentId) {
return;
}
const streamPosition = mediaStatus.streamPosition || 0;
const playerState = mediaStatus.playerState || null;
// Report every 10 seconds OR immediately when playerState changes (pause/resume)
const positionChanged =
Math.abs(streamPosition - lastReportedProgressRef.current) >= 10;
const stateChanged = playerState !== lastReportedPlayerStateRef.current;
if (!positionChanged && !stateChanged) {
return;
}
const contentId = mediaStatus.mediaInfo.contentId;
// Generate a new PlaySessionId when the content changes
if (contentId !== lastContentIdRef.current) {
// Use Math.random()-based UUID v4 (React Native lacks global crypto)
playSessionIdRef.current = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
(c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
);
lastContentIdRef.current = contentId;
}
const positionTicks = Math.floor(streamPosition * 10000000);
const isPaused = mediaStatus.playerState === "paused";
const streamUrl = mediaStatus.mediaInfo.contentUrl || "";
const isTranscoding = /m3u8/i.test(streamUrl);
const progressInfo: PlaybackProgressInfo = {
ItemId: contentId,
PositionTicks: positionTicks,
IsPaused: isPaused,
PlayMethod: isTranscoding ? "Transcode" : "DirectStream",
PlaySessionId: playSessionIdRef.current || contentId,
};
getPlaystateApi(api)
.reportPlaybackProgress({ playbackProgressInfo: progressInfo })
.then(() => {
lastReportedProgressRef.current = streamPosition;
lastReportedPlayerStateRef.current = playerState;
})
.catch((error) => {
console.error("Failed to report Chromecast progress:", error);
});
}, [
api,
user?.Id,
mediaStatus?.streamPosition,
mediaStatus?.mediaInfo?.contentId,
mediaStatus?.playerState,
mediaStatus?.mediaInfo?.contentUrl,
]);
// Android requires the cast button to be present for startDiscovery to work // Android requires the cast button to be present for startDiscovery to work
const AndroidCastButton = useCallback( const AndroidCastButton = useCallback(
@@ -164,92 +43,50 @@ export function Chromecast({
[Platform.OS], [Platform.OS],
); );
// Handle press - show connection menu when connected, otherwise show cast dialog
const handlePress = useCallback(() => {
if (isConnected) {
if (mediaStatus?.currentItemId) {
// Media is playing - navigate to full player
router.push("/casting-player");
} else {
// Connected but no media - show connection menu
setShowConnectionMenu(true);
}
} else {
// Not connected - show cast dialog
CastContext.showCastDialog();
}
}, [isConnected, mediaStatus?.currentItemId]);
// Handle disconnect from Chromecast
const handleDisconnect = useCallback(async () => {
try {
const sessionManager = GoogleCast.getSessionManager();
await sessionManager.endCurrentSession(true);
} catch (error) {
console.error("[Chromecast] Disconnect error:", error);
}
}, []);
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
return ( return (
<> <Pressable
<Pressable className='mr-4' onPress={handlePress} {...props}> className='mr-4'
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
{...props}
>
<AndroidCastButton /> <AndroidCastButton />
<Feather <Feather name='cast' size={22} color={"white"} />
name='cast'
size={22}
color={isConnected ? "#a855f7" : "white"}
/>
</Pressable> </Pressable>
<ChromecastConnectionMenu
visible={showConnectionMenu}
onClose={() => setShowConnectionMenu(false)}
onDisconnect={handleDisconnect}
/>
</>
); );
} }
if (background === "transparent") if (background === "transparent")
return ( return (
<>
<RoundButton <RoundButton
size='large' size='large'
className='mr-2' className='mr-2'
background={false} background={false}
onPress={handlePress} onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
{...props} {...props}
> >
<AndroidCastButton /> <AndroidCastButton />
<Feather <Feather name='cast' size={22} color={"white"} />
name='cast'
size={22}
color={isConnected ? "#a855f7" : "white"}
/>
</RoundButton> </RoundButton>
<ChromecastConnectionMenu
visible={showConnectionMenu}
onClose={() => setShowConnectionMenu(false)}
onDisconnect={handleDisconnect}
/>
</>
); );
return ( return (
<> <RoundButton
<RoundButton size='large' onPress={handlePress} {...props}> size='large'
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
{...props}
>
<AndroidCastButton /> <AndroidCastButton />
<Feather <Feather name='cast' size={22} color={"white"} />
name='cast'
size={22}
color={isConnected ? "#a855f7" : "white"}
/>
</RoundButton> </RoundButton>
<ChromecastConnectionMenu
visible={showConnectionMenu}
onClose={() => setShowConnectionMenu(false)}
onDisconnect={handleDisconnect}
/>
</>
); );
} }

View File

@@ -6,8 +6,8 @@ import type {
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector";
import type { ThemeColors } from "@/hooks/useImageColorsReturn"; import type { ThemeColors } from "@/hooks/useImageColorsReturn";
import { BITRATES } from "./BitRateSheet";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";

View File

@@ -63,7 +63,6 @@ interface PlatformDropdownProps {
open?: boolean; open?: boolean;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
onOptionSelect?: (value?: any) => void; onOptionSelect?: (value?: any) => void;
disabled?: boolean;
expoUIConfig?: { expoUIConfig?: {
hostStyle?: any; hostStyle?: any;
}; };
@@ -214,9 +213,6 @@ const PlatformDropdownComponent = ({
onOpenChange: controlledOnOpenChange, onOpenChange: controlledOnOpenChange,
onOptionSelect, onOptionSelect,
expoUIConfig, expoUIConfig,
// Aliased to avoid shadowing the module-level `disabled` SwiftUI modifier
// (from @expo/ui/swift-ui/modifiers) used by the iOS <Menu> renderer below.
disabled: isDisabled,
bottomSheetConfig, bottomSheetConfig,
}: PlatformDropdownProps) => { }: PlatformDropdownProps) => {
const { showModal, hideModal, isVisible } = useGlobalModal(); const { showModal, hideModal, isVisible } = useGlobalModal();
@@ -269,13 +265,6 @@ const PlatformDropdownComponent = ({
}, [isVisible, controlledOpen, controlledOnOpenChange]); }, [isVisible, controlledOpen, controlledOnOpenChange]);
if (Platform.OS === "ios" && !Platform.isTV) { if (Platform.OS === "ios" && !Platform.isTV) {
if (isDisabled) {
return (
<View style={{ opacity: 0.5 }} pointerEvents='none'>
{trigger || <Text className='text-white'>Open Menu</Text>}
</View>
);
}
// Pin the wrapper to the measured trigger size. @expo/ui's <Host> (SDK 55) // Pin the wrapper to the measured trigger size. @expo/ui's <Host> (SDK 55)
// fills its parent and reports its own size via setStyleSize, so it can't // fills its parent and reports its own size via setStyleSize, so it can't
// size itself to content. If the wrapper has no size, the Host's `flex: 1` // size itself to content. If the wrapper has no size, the Host's `flex: 1`
@@ -428,14 +417,8 @@ const PlatformDropdownComponent = ({
}; };
return ( return (
<TouchableOpacity <TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
onPress={handlePress}
activeOpacity={0.7}
disabled={isDisabled}
>
<View style={isDisabled ? { opacity: 0.5 } : undefined}>
{trigger || <Text className='text-white'>Open Menu</Text>} {trigger || <Text className='text-white'>Open Menu</Text>}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}; };

View File

@@ -8,9 +8,8 @@ import { useTranslation } from "react-i18next";
import { Alert, Platform, TouchableOpacity, View } from "react-native"; import { Alert, Platform, TouchableOpacity, View } from "react-native";
import CastContext, { import CastContext, {
CastButton, CastButton,
MediaPlayerState, MediaStreamType,
PlayServicesState, PlayServicesState,
useCastDevice,
useMediaStatus, useMediaStatus,
useRemoteMediaClient, useRemoteMediaClient,
} from "react-native-google-cast"; } from "react-native-google-cast";
@@ -33,8 +32,12 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { loadCastMedia } from "@/utils/casting/castLoad"; import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { runtimeTicksToMinutes } from "@/utils/time"; import { runtimeTicksToMinutes } from "@/utils/time";
import { chromecast } from "../utils/profiles/chromecast";
import { chromecasth265 } from "../utils/profiles/chromecasth265";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";
@@ -56,7 +59,6 @@ export const PlayButton: React.FC<Props> = ({
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
const { showActionSheetWithOptions } = useActionSheet(); const { showActionSheetWithOptions } = useActionSheet();
const client = useRemoteMediaClient(); const client = useRemoteMediaClient();
const castDevice = useCastDevice();
const mediaStatus = useMediaStatus(); const mediaStatus = useMediaStatus();
const { t } = useTranslation(); const { t } = useTranslation();
const { showModal, hideModal } = useGlobalModal(); const { showModal, hideModal } = useGlobalModal();
@@ -109,11 +111,7 @@ export const PlayButton: React.FC<Props> = ({
return; return;
} }
const options = [ const options = ["Chromecast", "Device", "Cancel"];
t("casting_player.chromecast"),
t("casting_player.device"),
t("casting_player.cancel"),
];
const cancelButtonIndex = 2; const cancelButtonIndex = 2;
showActionSheetWithOptions( showActionSheetWithOptions(
{ {
@@ -122,14 +120,9 @@ export const PlayButton: React.FC<Props> = ({
}, },
async (selectedIndex: number | undefined) => { async (selectedIndex: number | undefined) => {
if (!api) return; if (!api) return;
// Compare item IDs AND check if media is actually playing (not stopped/idle) const currentTitle = mediaStatus?.mediaInfo?.metadata?.title;
const currentContentId = mediaStatus?.mediaInfo?.contentId;
const isMediaActive =
mediaStatus?.playerState === MediaPlayerState.PLAYING ||
mediaStatus?.playerState === MediaPlayerState.PAUSED ||
mediaStatus?.playerState === MediaPlayerState.BUFFERING;
const isOpeningCurrentlyPlayingMedia = const isOpeningCurrentlyPlayingMedia =
isMediaActive && currentContentId && currentContentId === item?.Id; currentTitle && currentTitle === item?.Name;
switch (selectedIndex) { switch (selectedIndex) {
case 0: case 0:
@@ -137,8 +130,30 @@ export const PlayButton: React.FC<Props> = ({
if (state && state !== PlayServicesState.SUCCESS) { if (state && state !== PlayServicesState.SUCCESS) {
CastContext.showPlayServicesErrorDialog(state); CastContext.showPlayServicesErrorDialog(state);
} else { } else {
if (!api || !user?.Id || !item?.Id) { // Check if user wants H265 for Chromecast
console.warn("Missing parameters for Chromecast streaming"); const enableH265 = settings.enableH265ForChromecast;
// Validate required parameters before calling getStreamUrl
if (!api) {
console.warn("API not available for Chromecast streaming");
Alert.alert(
t("player.client_error"),
t("player.missing_parameters"),
);
return;
}
if (!user?.Id) {
console.warn(
"User not authenticated for Chromecast streaming",
);
Alert.alert(
t("player.client_error"),
t("player.missing_parameters"),
);
return;
}
if (!item?.Id) {
console.warn("Item not available for Chromecast streaming");
Alert.alert( Alert.alert(
t("player.client_error"), t("player.client_error"),
t("player.missing_parameters"), t("player.missing_parameters"),
@@ -146,28 +161,24 @@ export const PlayButton: React.FC<Props> = ({
return; return;
} }
const startPositionMs = // Get a new URL with the Chromecast device profile
(item.UserData?.PlaybackPositionTicks ?? 0) / 10000; try {
const data = await getStreamUrl({
const result = await loadCastMedia({
client,
device: castDevice,
api, api,
item, item,
deviceProfile: enableH265 ? chromecasth265 : chromecast,
startTimeTicks: item?.UserData?.PlaybackPositionTicks ?? 0,
userId: user.Id, userId: user.Id,
profileMode: settings.chromecastProfile,
maxBitrateSetting: settings.chromecastMaxBitrate,
options: {
audioStreamIndex: selectedOptions.audioIndex, audioStreamIndex: selectedOptions.audioIndex,
maxStreamingBitrate: selectedOptions.bitrate?.value,
mediaSourceId: selectedOptions.mediaSource?.Id,
subtitleStreamIndex: selectedOptions.subtitleIndex, subtitleStreamIndex: selectedOptions.subtitleIndex,
maxBitrate: selectedOptions.bitrate?.value,
mediaSourceId: selectedOptions.mediaSource?.Id ?? undefined,
startPositionMs,
},
}); });
if (!result.ok) { console.log("URL: ", data?.url, enableH265);
console.error("[PlayButton] cast load failed:", result.error);
if (!data?.url) {
console.warn("No URL returned from getStreamUrl", data);
Alert.alert( Alert.alert(
t("player.client_error"), t("player.client_error"),
t("player.could_not_create_stream_for_chromecast"), t("player.could_not_create_stream_for_chromecast"),
@@ -175,8 +186,85 @@ export const PlayButton: React.FC<Props> = ({
return; return;
} }
if (!isOpeningCurrentlyPlayingMedia) { // Calculate start time in seconds from playback position
router.push("/casting-player"); const startTimeSeconds =
(item?.UserData?.PlaybackPositionTicks ?? 0) / 10000000;
// Calculate stream duration in seconds from runtime
const streamDurationSeconds = item.RunTimeTicks
? item.RunTimeTicks / 10000000
: undefined;
client
.loadMedia({
mediaInfo: {
contentId: item.Id,
contentUrl: data?.url,
contentType: "video/mp4",
streamType: MediaStreamType.BUFFERED,
streamDuration: streamDurationSeconds,
metadata:
item.Type === "Episode"
? {
type: "tvShow",
title: item.Name || "",
episodeNumber: item.IndexNumber || 0,
seasonNumber: item.ParentIndexNumber || 0,
seriesTitle: item.SeriesName || "",
images: [
{
url: getParentBackdropImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
}
: item.Type === "Movie"
? {
type: "movie",
title: item.Name || "",
subtitle: item.Overview || "",
images: [
{
url: getPrimaryImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
}
: {
type: "generic",
title: item.Name || "",
subtitle: item.Overview || "",
images: [
{
url: getPrimaryImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
},
},
startTime: startTimeSeconds,
})
.then(() => {
// state is already set when reopening current media, so skip it here.
if (isOpeningCurrentlyPlayingMedia) {
return;
}
CastContext.showExpandedControls();
});
} catch (e) {
console.log(e);
} }
} }
}); });
@@ -192,7 +280,6 @@ export const PlayButton: React.FC<Props> = ({
}, [ }, [
item, item,
client, client,
castDevice,
settings, settings,
api, api,
user, user,

View File

@@ -1,12 +0,0 @@
/**
* Always-mounted host for the Chromecast autoplay watcher. Renders nothing —
* it exists only to run `useCastAutoplay` for the app's lifetime, so autoplay
* fires regardless of which screen is open.
*/
import { useCastAutoplay } from "@/hooks/useCastAutoplay";
export function CastAutoplayWatcher() {
useCastAutoplay();
return null;
}

View File

@@ -1,358 +0,0 @@
/**
* Unified Casting Mini Player
* Works with all supported casting protocols
*/
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router } from "expo-router";
import { useAtomValue } from "jotai";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import {
MediaPlayerState,
useCastDevice,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import Animated, {
SlideInDown,
SlideOutDown,
useSharedValue,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { CastTrickplayBubble } from "@/components/casting/player/CastTrickplayBubble";
import { Text } from "@/components/common/Text";
import { useCastPlayerItem } from "@/hooks/useCastPlayerItem";
import { useTrickplay } from "@/hooks/useTrickplay";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { formatTime, getPosterUrl } from "@/utils/casting/helpers";
import { CASTING_CONSTANTS } from "@/utils/casting/types";
import { msToTicks, ticksToSeconds } from "@/utils/time";
export const CastingMiniPlayer: React.FC = () => {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const insets = useSafeAreaInsets();
const castDevice = useCastDevice();
const mediaStatus = useMediaStatus();
const remoteMediaClient = useRemoteMediaClient();
const { currentItem } = useCastPlayerItem({ api, user, mediaStatus });
// Trickplay support - pass currentItem as BaseItemDto or null
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
currentItem || null,
);
const [trickplayTime, setTrickplayTime] = useState({
hours: 0,
minutes: 0,
seconds: 0,
});
const isScrubbing = useRef(false);
// Slider shared values
const sliderProgress = useSharedValue(0);
const sliderMin = useSharedValue(0);
const sliderMax = useSharedValue(100);
// Live progress state that updates every second when playing
const [liveProgress, setLiveProgress] = useState(
mediaStatus?.streamPosition || 0,
);
// Track baseline for elapsed-time computation
const baselinePositionRef = useRef(mediaStatus?.streamPosition || 0);
const baselineTimestampRef = useRef(Date.now());
// Sync live progress with mediaStatus and poll every second when playing
useEffect(() => {
// Resync baseline whenever mediaStatus reports a new position
if (mediaStatus?.streamPosition !== undefined) {
baselinePositionRef.current = mediaStatus.streamPosition;
baselineTimestampRef.current = Date.now();
setLiveProgress(mediaStatus.streamPosition);
}
// Update based on elapsed real time when playing
const interval = setInterval(() => {
if (mediaStatus?.playerState === MediaPlayerState.PLAYING) {
const elapsed =
((Date.now() - baselineTimestampRef.current) *
(mediaStatus.playbackRate || 1)) /
1000;
setLiveProgress(baselinePositionRef.current + elapsed);
} else if (mediaStatus?.streamPosition !== undefined) {
// Sync with actual position when paused/buffering
baselinePositionRef.current = mediaStatus.streamPosition;
baselineTimestampRef.current = Date.now();
setLiveProgress(mediaStatus.streamPosition);
}
}, 1000);
return () => clearInterval(interval);
}, [
mediaStatus?.playerState,
mediaStatus?.streamPosition,
mediaStatus?.playbackRate,
]);
const progress = liveProgress * 1000; // Convert to ms
const duration = (mediaStatus?.mediaInfo?.streamDuration || 0) * 1000;
const isPlaying = mediaStatus?.playerState === MediaPlayerState.PLAYING;
// Update slider max value when duration changes
useEffect(() => {
if (duration > 0) {
sliderMax.value = duration;
}
}, [duration, sliderMax]);
// Sync slider progress with live progress (when not scrubbing)
useEffect(() => {
if (!isScrubbing.current && progress >= 0) {
sliderProgress.value = progress;
}
}, [progress, sliderProgress]);
// For episodes, use series poster; for other content, use item poster
const posterUrl = useMemo(() => {
if (!api?.basePath || !currentItem) return null;
if (
currentItem.Type === "Episode" &&
currentItem.SeriesId &&
currentItem.ParentIndexNumber !== undefined &&
currentItem.SeasonId
) {
// Build series poster URL using SeriesId and series-level image tag
const imageTag = currentItem.SeriesPrimaryImageTag || "";
const tagParam = imageTag ? `&tag=${imageTag}` : "";
return `${api.basePath}/Items/${currentItem.SeriesId}/Images/Primary?fillHeight=120&fillWidth=80&quality=96${tagParam}`;
}
// For non-episodes, use item's own poster
return getPosterUrl(
api.basePath,
currentItem.Id,
currentItem.ImageTags?.Primary,
80,
120,
);
}, [api?.basePath, currentItem]);
// Hide mini player when:
// - No cast device connected
// - No media info (currentItem)
// - No media status
// - Media is stopped (IDLE state)
// - Media is unknown state
const playerState = mediaStatus?.playerState;
const isMediaStopped = playerState === MediaPlayerState.IDLE;
if (!castDevice || !currentItem || !mediaStatus || isMediaStopped) {
return null;
}
const protocolColor = "#a855f7"; // Streamyfin purple
const TAB_BAR_HEIGHT = 80; // Standard tab bar height
const handlePress = () => {
router.push("/casting-player");
};
const handleTogglePlayPause = () => {
if (isPlaying) {
remoteMediaClient?.pause()?.catch((error: unknown) => {
console.error("[CastingMiniPlayer] Pause error:", error);
});
} else {
remoteMediaClient?.play()?.catch((error: unknown) => {
console.error("[CastingMiniPlayer] Play error:", error);
});
}
};
return (
<Animated.View
entering={SlideInDown.duration(CASTING_CONSTANTS.ANIMATION_DURATION)}
exiting={SlideOutDown.duration(CASTING_CONSTANTS.ANIMATION_DURATION)}
style={{
position: "absolute",
bottom: TAB_BAR_HEIGHT + insets.bottom,
left: 0,
right: 0,
backgroundColor: "#1a1a1a",
borderTopWidth: 1,
borderTopColor: "#333",
zIndex: 100,
}}
>
{/* Interactive progress slider with trickplay */}
<View style={{ paddingHorizontal: 8, paddingTop: 4 }}>
<Slider
style={{ width: "100%", height: 20 }}
progress={sliderProgress}
minimumValue={sliderMin}
maximumValue={sliderMax}
theme={{
maximumTrackTintColor: "#333",
minimumTrackTintColor: protocolColor,
bubbleBackgroundColor: protocolColor,
bubbleTextColor: "#fff",
}}
onSlidingStart={() => {
isScrubbing.current = true;
}}
onValueChange={(value) => {
// Calculate trickplay preview
const progressInTicks = msToTicks(value);
calculateTrickplayUrl(progressInTicks);
// Update time display for trickplay bubble
const progressInSeconds = Math.floor(
ticksToSeconds(progressInTicks),
);
const hours = Math.floor(progressInSeconds / 3600);
const minutes = Math.floor((progressInSeconds % 3600) / 60);
const seconds = progressInSeconds % 60;
setTrickplayTime({ hours, minutes, seconds });
}}
onSlidingComplete={(value) => {
isScrubbing.current = false;
// Seek to the position (value is in milliseconds, convert to seconds)
const positionSeconds = value / 1000;
if (remoteMediaClient && duration > 0) {
remoteMediaClient
.seek({ position: positionSeconds })
.catch((error) => {
console.error("[Mini Player] Seek error:", error);
});
}
}}
renderBubble={() => (
<CastTrickplayBubble
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
trickplayTime={trickplayTime}
tileWidth={190}
/>
)}
bubbleMaxWidth={190}
bubbleWidth={190}
bubbleTranslateY={-20}
sliderHeight={3}
thumbWidth={14}
panHitSlop={{ top: 20, bottom: 20, left: 5, right: 5 }}
/>
</View>
<Pressable onPress={handlePress}>
{/* Content */}
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 12,
paddingTop: 6,
gap: 12,
}}
>
{/* Poster */}
{posterUrl && (
<Image
source={{ uri: posterUrl }}
style={{
width: 40,
height: 60,
borderRadius: 4,
}}
contentFit='cover'
/>
)}
{/* Info */}
<View style={{ flex: 1 }}>
<Text
style={{
color: "white",
fontSize: 14,
fontWeight: "600",
}}
numberOfLines={1}
>
{currentItem.Name}
</Text>
{currentItem.SeriesName && (
<Text
style={{
color: "#999",
fontSize: 12,
}}
numberOfLines={1}
>
{currentItem.SeriesName}
</Text>
)}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
marginTop: 2,
}}
>
<Ionicons name='tv' size={12} color={protocolColor} />
<Text
style={{
color: protocolColor,
fontSize: 11,
}}
numberOfLines={1}
>
{castDevice.friendlyName || "Chromecast"}
</Text>
<Text
style={{
color: "#666",
fontSize: 11,
}}
>
{formatTime(progress)} / {formatTime(duration)}
</Text>
</View>
</View>
{/* Stop button */}
<Pressable
onPress={(e) => {
e.stopPropagation();
remoteMediaClient?.stop()?.catch((error: unknown) => {
console.error("[CastingMiniPlayer] Stop error:", error);
});
}}
style={{ padding: 8 }}
>
<Ionicons name='stop' size={24} color='white' />
</Pressable>
{/* Play/Pause button */}
<Pressable
onPress={(e) => {
e.stopPropagation();
handleTogglePlayPause();
}}
style={{ padding: 8 }}
>
<Ionicons
name={isPlaying ? "pause" : "play"}
size={28}
color='white'
/>
</Pressable>
</View>
</Pressable>
</Animated.View>
);
};

View File

@@ -1,175 +0,0 @@
/**
* Casting Player Episode Controls
* Fixed control row: episode list, previous, next, stop.
* Episode-specific buttons (list / previous / next) are conditional;
* Stop is always rendered so movies still get a Stop button.
*/
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { ImperativeRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Pressable, View } from "react-native";
import type { RemoteMediaClient } from "react-native-google-cast";
import { Text } from "@/components/common/Text";
interface CastPlayerEpisodeControlsProps {
/** Bottom safe-area inset, used to offset the fixed control row. */
insetBottom: number;
/** Id of the currently playing episode. */
currentItemId: BaseItemDto["Id"];
/** Full episode list for the series. */
episodes: BaseItemDto[];
/** Next episode in the list, or null if none. */
nextEpisode: BaseItemDto | null;
/** Remote media client, or null when no session. */
remoteMediaClient: RemoteMediaClient | null;
/** Open the episode list modal. */
onPressEpisodes: () => void;
/** Whether the current item exposes chapter markers. */
hasChapters: boolean;
/** Open the chapter list modal. */
onPressChapters: () => void;
/** Load a different episode on the Chromecast. */
loadEpisode: (episode: BaseItemDto) => Promise<void>;
/** Expo Router instance for navigation on stop. */
router: ImperativeRouter;
}
export function CastPlayerEpisodeControls({
insetBottom,
currentItemId,
episodes,
nextEpisode,
remoteMediaClient,
onPressEpisodes,
hasChapters,
onPressChapters,
loadEpisode,
router,
}: CastPlayerEpisodeControlsProps) {
const { t } = useTranslation();
const hasEpisodeList = episodes.length > 0;
const hasPrevious = episodes.findIndex((ep) => ep.Id === currentItemId) > 0;
const hasNext = nextEpisode != null;
// Count of buttons actually rendered (Stop is always rendered).
const buttonCount =
1 +
(hasEpisodeList ? 1 : 0) +
(hasChapters ? 1 : 0) +
(hasPrevious ? 1 : 0) +
(hasNext ? 1 : 0);
// When Stop is the only button (movies), render it full-width with a label.
const isLoneStop = buttonCount === 1;
// Each button stretches evenly only when the row holds more than one;
// a lone Stop button keeps its intrinsic size and stays centered.
const buttonStyle = {
...(buttonCount > 1 ? { flex: 1 } : {}),
backgroundColor: "#1a1a1a",
padding: 12,
borderRadius: 12,
flexDirection: "row" as const,
justifyContent: "center" as const,
alignItems: "center" as const,
};
return (
<View
style={{
position: "absolute",
bottom: insetBottom + 200,
left: 0,
right: 0,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 16,
paddingHorizontal: 20,
}}
>
{/* Episodes button - only rendered when an episode list exists (not for movies) */}
{hasEpisodeList && (
<Pressable onPress={onPressEpisodes} style={buttonStyle}>
<Ionicons name='list' size={22} color='white' />
</Pressable>
)}
{/* Chapter list button - rendered for both episodes and movies when chapters exist */}
{hasChapters && (
<Pressable onPress={onPressChapters} style={buttonStyle}>
<Ionicons name='bookmarks' size={22} color='white' />
</Pressable>
)}
{/* Previous episode button - only rendered when a previous episode exists */}
{hasPrevious && (
<Pressable
onPress={async () => {
const currentIndex = episodes.findIndex(
(ep) => ep.Id === currentItemId,
);
if (currentIndex > 0) {
await loadEpisode(episodes[currentIndex - 1]);
}
}}
style={buttonStyle}
>
<Ionicons name='play-skip-back' size={22} color='white' />
</Pressable>
)}
{/* Next episode button - only rendered when a next episode exists */}
{hasNext && (
<Pressable
onPress={async () => {
if (nextEpisode) {
await loadEpisode(nextEpisode);
}
}}
style={buttonStyle}
>
<Ionicons name='play-skip-forward' size={22} color='white' />
</Pressable>
)}
{/* Stop playback button - always rendered; stops media but stays connected to Chromecast */}
<Pressable
onPress={async () => {
try {
// Stop the current media playback (don't disconnect from Chromecast)
if (remoteMediaClient) {
await remoteMediaClient.stop();
}
// Navigate back/close the player (mini player will disappear since no media is playing)
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
} catch (error) {
console.error("[Casting Player] Error stopping playback:", error);
// Navigate anyway
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
}
}}
style={[buttonStyle, isLoneStop && { flex: 1, gap: 8 }]}
>
<Ionicons name='stop-circle' size={22} color='white' />
{isLoneStop && (
<Text style={{ color: "white", fontSize: 15, fontWeight: "600" }}>
{t("casting_player.stop")}
</Text>
)}
</Pressable>
</View>
);
}

View File

@@ -1,94 +0,0 @@
/**
* Casting Player Header
* Fixed top bar: dismiss button, connection indicator, settings button.
*/
import { Ionicons } from "@expo/vector-icons";
import type { TFunction } from "i18next";
import { Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
interface CastPlayerHeaderProps {
/** Top safe-area inset, used to offset the fixed header. */
insetTop: number;
/** Streamyfin protocol accent color. */
protocolColor: string;
/** Friendly name of the connected cast device, or null. */
currentDevice: string | null;
/** Translation function. */
t: TFunction;
/** Dismiss the casting player modal. */
onDismiss: () => void;
/** Open the device sheet (connection indicator press). */
onPressConnectionIndicator: () => void;
/** Open the settings menu. */
onPressSettings: () => void;
}
export function CastPlayerHeader({
insetTop,
protocolColor,
currentDevice,
t,
onDismiss,
onPressConnectionIndicator,
onPressSettings,
}: CastPlayerHeaderProps) {
return (
<View
style={{
position: "absolute",
top: insetTop + 8,
left: 20,
right: 20,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
zIndex: 100,
}}
>
<Pressable onPress={onDismiss} style={{ padding: 8, marginLeft: -8 }}>
<Ionicons name='chevron-down' size={32} color='white' />
</Pressable>
{/* Connection indicator */}
<Pressable
onPress={onPressConnectionIndicator}
style={{
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: "#1a1a1a",
borderRadius: 16,
}}
>
<View
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: protocolColor,
}}
/>
<Text
style={{
color: protocolColor,
fontSize: 14,
fontWeight: "500",
}}
>
{currentDevice || t("casting_player.unknown_device")}
</Text>
</Pressable>
<Pressable
onPress={onPressSettings}
style={{ padding: 8, marginRight: -8 }}
>
<Ionicons name='settings-outline' size={24} color='white' />
</Pressable>
</View>
);
}

View File

@@ -1,176 +0,0 @@
/**
* Casting Player Poster
* Poster image with empty-state fallback, skip intro/credits bar, and buffering overlay.
*/
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import type { TFunction } from "i18next";
import { ActivityIndicator, Pressable, View } from "react-native";
import {
MediaPlayerState,
type MediaStatus,
type RemoteMediaClient,
} from "react-native-google-cast";
import type { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
import { Text } from "@/components/common/Text";
type ChromecastSegments = ReturnType<typeof useChromecastSegments>;
interface CastPlayerPosterProps {
/** Poster image URL, or null when unavailable. */
posterUrl: string | null;
/** Whether the cast media is currently buffering. */
isBuffering: boolean;
/** The current playback segment (intro/credits/etc.), or null. */
currentSegment: ChromecastSegments["currentSegment"];
/** Skip the intro segment. */
skipIntro: ChromecastSegments["skipIntro"];
/** Skip the credits segment. */
skipCredits: ChromecastSegments["skipCredits"];
/** Skip the current generic segment. */
skipSegment: ChromecastSegments["skipSegment"];
/** The remote media client, or null when no session. */
remoteMediaClient: RemoteMediaClient | null;
/** Raw Chromecast media status. */
mediaStatus: MediaStatus | null;
/** Theme accent color. */
protocolColor: string;
/** Translation function. */
t: TFunction;
}
export function CastPlayerPoster({
posterUrl,
isBuffering,
currentSegment,
skipIntro,
skipCredits,
skipSegment,
remoteMediaClient,
mediaStatus,
protocolColor,
t,
}: CastPlayerPosterProps) {
return (
<View
style={{
alignItems: "center",
marginBottom: 40,
}}
>
<View
style={{
width: 280,
height: 420,
borderRadius: 12,
overflow: "hidden",
position: "relative",
}}
>
{posterUrl ? (
<Image
source={{ uri: posterUrl }}
style={{ width: "100%", height: "100%" }}
contentFit='cover'
/>
) : (
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "#1a1a1a",
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons name='film-outline' size={64} color='#333' />
</View>
)}
{/* Skip intro/credits bar at bottom of poster */}
{currentSegment && (
<Pressable
onPress={async () => {
if (!remoteMediaClient) return;
try {
const seekFn = async (positionMs: number) => {
if (
mediaStatus?.playerState === MediaPlayerState.PLAYING ||
mediaStatus?.playerState === MediaPlayerState.PAUSED
) {
await remoteMediaClient.seek({
position: positionMs / 1000,
});
}
};
if (currentSegment.type === "intro") {
await skipIntro(seekFn);
} else if (currentSegment.type === "credits") {
await skipCredits(seekFn);
} else {
await skipSegment(seekFn);
}
} catch (error) {
console.error("[Casting Player] Skip error:", error);
}
}}
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
backgroundColor: protocolColor,
paddingVertical: 12,
paddingHorizontal: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
>
<Ionicons name='play-skip-forward' size={18} color='white' />
<Text
style={{
color: "white",
fontSize: 14,
fontWeight: "600",
}}
>
{t(
`player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`,
)}
</Text>
</Pressable>
)}
{/* Buffering overlay */}
{isBuffering && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.7)",
justifyContent: "center",
alignItems: "center",
}}
>
<ActivityIndicator size='large' color={protocolColor} />
<Text
style={{
color: "white",
fontSize: 16,
marginTop: 16,
}}
>
{t("casting_player.buffering")}
</Text>
</View>
)}
</View>
</View>
);
}

View File

@@ -1,163 +0,0 @@
/**
* Casting Player Progress Bar
* Progress slider with trickplay preview bubble and current/end time display.
*/
import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models";
import type { TFunction } from "i18next";
import { Text, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import type { RemoteMediaClient } from "react-native-google-cast";
import type { SharedValue } from "react-native-reanimated";
import { CastTrickplayBubble } from "@/components/casting/player/CastTrickplayBubble";
import { ChapterTicks } from "@/components/chapters/ChapterTicks";
import type { useTrickplay } from "@/hooks/useTrickplay";
import { calculateEndingTime, formatTime } from "@/utils/casting/helpers";
import { chapterMarkers } from "@/utils/chapters";
import { msToTicks, ticksToSeconds } from "@/utils/time";
type TrickplayReturn = ReturnType<typeof useTrickplay>;
interface CastPlayerProgressBarProps {
/** Shared value tracking the slider progress, in milliseconds. */
sliderProgress: SharedValue<number>;
/** Shared value for the slider minimum, in milliseconds. */
sliderMin: SharedValue<number>;
/** Shared value for the slider maximum, in milliseconds. */
sliderMax: SharedValue<number>;
/** Mutable ref flag set true while the user is scrubbing. */
isScrubbing: { current: boolean };
/** Trickplay time display state for the bubble. */
trickplayTime: { hours: number; minutes: number; seconds: number };
/** Updates the trickplay time display state. */
setTrickplayTime: (time: {
hours: number;
minutes: number;
seconds: number;
}) => void;
/** Current trickplay image URL/coordinates, or null. */
trickPlayUrl: TrickplayReturn["trickPlayUrl"];
/** Computes the trickplay URL for a given progress in ticks. */
calculateTrickplayUrl: TrickplayReturn["calculateTrickplayUrl"];
/** Parsed trickplay metadata, or null. */
trickplayInfo: TrickplayReturn["trickplayInfo"];
/** Current playback progress, in seconds. */
progress: number;
/** Total media duration, in seconds. */
duration: number;
/** Remote media client, or null when no session. */
remoteMediaClient: RemoteMediaClient | null;
/** Theme color used for the slider track and bubbles. */
protocolColor: string;
/** Chapter markers for the current item, or null/undefined if none. */
chapters?: ChapterInfo[] | null;
/** Translation function. */
t: TFunction;
}
export function CastPlayerProgressBar({
sliderProgress,
sliderMin,
sliderMax,
isScrubbing,
trickplayTime,
setTrickplayTime,
trickPlayUrl,
calculateTrickplayUrl,
trickplayInfo,
progress,
duration,
remoteMediaClient,
protocolColor,
chapters,
t,
}: CastPlayerProgressBarProps) {
return (
<>
{/* Progress slider with trickplay preview */}
<View style={{ marginTop: 8, height: 40 }}>
<Slider
style={{ width: "100%", height: 40 }}
progress={sliderProgress}
minimumValue={sliderMin}
maximumValue={sliderMax}
theme={{
maximumTrackTintColor: "#333",
minimumTrackTintColor: protocolColor,
bubbleBackgroundColor: protocolColor,
bubbleTextColor: "#fff",
}}
onSlidingStart={() => {
isScrubbing.current = true;
}}
onValueChange={(value) => {
// Calculate trickplay preview
const progressInTicks = msToTicks(value);
calculateTrickplayUrl(progressInTicks);
// Update time display for trickplay bubble
const progressInSeconds = Math.floor(
ticksToSeconds(progressInTicks),
);
const hours = Math.floor(progressInSeconds / 3600);
const minutes = Math.floor((progressInSeconds % 3600) / 60);
const seconds = progressInSeconds % 60;
setTrickplayTime({ hours, minutes, seconds });
}}
onSlidingComplete={(value) => {
isScrubbing.current = false;
// Seek to the position (value is in milliseconds, convert to seconds)
const positionSeconds = value / 1000;
if (remoteMediaClient && duration > 0) {
remoteMediaClient
.seek({ position: positionSeconds })
.catch((error) => {
console.error("[Casting Player] Seek error:", error);
});
}
}}
renderBubble={() => (
<CastTrickplayBubble
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
trickplayTime={trickplayTime}
tileWidth={220}
/>
)}
bubbleMaxWidth={220}
bubbleWidth={220}
bubbleTranslateY={-20}
sliderHeight={6}
thumbWidth={16}
panHitSlop={{ top: 12, bottom: 12, left: 10, right: 10 }}
/>
<ChapterTicks
markers={chapterMarkers(chapters, duration * 1000)}
height={4}
color='#cccccc'
/>
</View>
{/* Time display */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 24,
}}
>
<Text style={{ color: "#999", fontSize: 13 }}>
{formatTime(progress * 1000)}
</Text>
<Text style={{ color: "#999", fontSize: 13 }}>
{t("casting_player.ending_at", {
time: calculateEndingTime(progress * 1000, duration * 1000),
})}
</Text>
<Text style={{ color: "#999", fontSize: 13 }}>
{formatTime(duration * 1000)}
</Text>
</View>
</>
);
}

View File

@@ -1,72 +0,0 @@
/**
* Casting Player Title Area
* Fixed title bar: item title and optional grey episode/season info.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { TFunction } from "i18next";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { truncateTitle } from "@/utils/casting/helpers";
interface CastPlayerTitleProps {
/** Top safe-area inset, used to offset the fixed title area. */
insetTop: number;
/** The currently playing item. */
currentItem: BaseItemDto;
/** Translation function. */
t: TFunction;
}
export function CastPlayerTitle({
insetTop,
currentItem,
t,
}: CastPlayerTitleProps) {
return (
<View
style={{
position: "absolute",
top: insetTop + 50,
left: 0,
right: 0,
zIndex: 95,
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.8)",
paddingVertical: 16,
paddingHorizontal: 20,
}}
>
{/* Title */}
<Text
style={{
color: "white",
fontSize: 20,
fontWeight: "700",
textAlign: "center",
marginBottom: 6,
}}
>
{truncateTitle(currentItem.Name || t("casting_player.unknown"), 50)}
</Text>
{/* Grey episode/season info */}
{currentItem.Type === "Episode" &&
currentItem.ParentIndexNumber !== undefined &&
currentItem.IndexNumber !== undefined && (
<Text
style={{
color: "#999",
fontSize: 15,
textAlign: "center",
}}
>
{t("casting_player.season_episode_format", {
season: currentItem.ParentIndexNumber,
episode: currentItem.IndexNumber,
})}
</Text>
)}
</View>
);
}

View File

@@ -1,122 +0,0 @@
/**
* Casting Player Transport Controls
* Playback transport row: rewind, play/pause, forward.
*/
import { Ionicons } from "@expo/vector-icons";
import { Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
interface CastPlayerTransportControlsProps {
/** Whether playback is currently playing. */
isPlaying: boolean;
/** Toggle play/pause on the Chromecast. */
togglePlayPause: () => Promise<void>;
/** Skip backward by the given number of seconds. */
skipBackward: (seconds: number) => Promise<void>;
/** Skip forward by the given number of seconds. */
skipForward: (seconds: number) => Promise<void>;
/** Configured rewind skip time in seconds, shown on the rewind button. */
rewindSkipTime: number | null | undefined;
/** Configured forward skip time in seconds, shown on the forward button. */
forwardSkipTime: number | null | undefined;
/** Accent color used for the play/pause button background. */
protocolColor: string;
}
export function CastPlayerTransportControls({
isPlaying,
togglePlayPause,
skipBackward,
skipForward,
rewindSkipTime,
forwardSkipTime,
protocolColor,
}: CastPlayerTransportControlsProps) {
return (
<View
style={{
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 32,
marginBottom: 24,
}}
>
{/* Rewind (use settings) */}
<Pressable
onPress={() => skipBackward(rewindSkipTime ?? 10)}
style={{
position: "relative",
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons
name='refresh-outline'
size={48}
color='white'
style={{ transform: [{ scaleY: -1 }, { rotate: "180deg" }] }}
/>
{rewindSkipTime != null && (
<Text
style={{
position: "absolute",
color: "white",
fontSize: 15,
fontWeight: "bold",
bottom: 10,
}}
>
{rewindSkipTime}
</Text>
)}
</Pressable>
{/* Play/Pause */}
<Pressable
onPress={togglePlayPause}
style={{
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: protocolColor,
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons
name={isPlaying ? "pause" : "play"}
size={36}
color='white'
style={{ marginLeft: isPlaying ? 0 : 4 }}
/>
</Pressable>
{/* Forward (use settings) */}
<Pressable
onPress={() => skipForward(forwardSkipTime ?? 10)}
style={{
position: "relative",
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons name='refresh-outline' size={48} color='white' />
{forwardSkipTime != null && (
<Text
style={{
position: "absolute",
color: "white",
fontSize: 15,
fontWeight: "bold",
bottom: 10,
}}
>
{forwardSkipTime}
</Text>
)}
</Pressable>
</View>
);
}

View File

@@ -1,110 +0,0 @@
/**
* Shared scrub-preview bubble for the casting progress bars.
*
* The slider (`react-native-awesome-slider`) sizes, centres and clamps this
* bubble on the thumb via its `bubbleMaxWidth` / `bubbleWidth` props. This
* component therefore does NO horizontal positioning — it only anchors itself
* vertically (`bottom: 0`, growing upward) so it sits above the progress bar.
*/
import { Image } from "expo-image";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import type { useTrickplay } from "@/hooks/useTrickplay";
import { formatTrickplayTime } from "@/utils/casting/helpers";
type TrickplayReturn = ReturnType<typeof useTrickplay>;
interface CastTrickplayBubbleProps {
/** Current trickplay image URL/coordinates, or null. */
trickPlayUrl: TrickplayReturn["trickPlayUrl"];
/** Parsed trickplay metadata, or null. */
trickplayInfo: TrickplayReturn["trickplayInfo"];
/** Scrub time to display. */
trickplayTime: { hours: number; minutes: number; seconds: number };
/** Trickplay tile width in px (220 main player, 140 mini-player). */
tileWidth: number;
}
export function CastTrickplayBubble({
trickPlayUrl,
trickplayInfo,
trickplayTime,
tileWidth,
}: CastTrickplayBubbleProps) {
const timeText = (
<Text
style={{
color: "#fff",
fontSize: 13,
fontWeight: "600",
textAlign: "center",
textShadowColor: "rgba(0, 0, 0, 0.85)",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
}}
>
{formatTrickplayTime(trickplayTime)}
</Text>
);
// Anchored to the bottom of the slider-positioned container, growing upward,
// and filling the container width (left/right: 0) so it stays centred on the
// thumb. No horizontal maths here — the slider owns horizontal placement.
if (!trickPlayUrl || !trickplayInfo) {
return (
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
alignItems: "center",
}}
>
{timeText}
</View>
);
}
const { x, y, url } = trickPlayUrl;
const tileHeight = tileWidth / (trickplayInfo.aspectRatio ?? 1.78);
return (
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
alignItems: "center",
gap: 4,
}}
>
{timeText}
<View
style={{
width: tileWidth,
height: tileHeight,
borderRadius: 8,
overflow: "hidden",
backgroundColor: "#1a1a1a",
}}
>
<Image
cachePolicy='memory-disk'
style={{
width: tileWidth * (trickplayInfo.data?.TileWidth ?? 1),
height: tileHeight * (trickplayInfo.data?.TileHeight ?? 1),
transform: [
{ translateX: -x * tileWidth },
{ translateY: -y * tileHeight },
],
}}
source={{ uri: url }}
contentFit='cover'
/>
</View>
</View>
);
}

View File

@@ -1,321 +0,0 @@
/**
* Chromecast Connection Menu
* Shows device info, volume control, and disconnect option
* Simple menu for when connected but not actively controlling playback
*/
import { Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useCastDevice, useCastSession } from "react-native-google-cast";
import { useSharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
interface ChromecastConnectionMenuProps {
visible: boolean;
onClose: () => void;
onDisconnect?: () => Promise<void>;
}
export const ChromecastConnectionMenu: React.FC<
ChromecastConnectionMenuProps
> = ({ visible, onClose, onDisconnect }) => {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const castDevice = useCastDevice();
const castSession = useCastSession();
// Volume state - use refs to avoid triggering re-renders during sliding
const [displayVolume, setDisplayVolume] = useState(50);
const [isMuted, setIsMuted] = useState(false);
const isMutedRef = useRef(false);
const volumeValue = useSharedValue(50);
const minimumValue = useSharedValue(0);
const maximumValue = useSharedValue(100);
const isSliding = useRef(false);
const lastSetVolume = useRef(50);
const protocolColor = "#a855f7";
// Get initial volume and mute state when menu opens
useEffect(() => {
if (!visible || !castSession) return;
// Get initial states
const fetchInitialState = async () => {
try {
const vol = await castSession.getVolume();
if (vol !== undefined) {
const percent = Math.round(vol * 100);
setDisplayVolume(percent);
volumeValue.value = percent;
lastSetVolume.current = percent;
}
const muted = await castSession.isMute();
isMutedRef.current = muted;
setIsMuted(muted);
} catch {
// Ignore errors
}
};
fetchInitialState();
// Poll for external volume changes (physical buttons) - only when not sliding
const interval = setInterval(async () => {
if (isSliding.current) return;
try {
const vol = await castSession.getVolume();
if (vol !== undefined) {
const percent = Math.round(vol * 100);
// Only update if external change detected (not our own change)
if (Math.abs(percent - lastSetVolume.current) > 2) {
setDisplayVolume(percent);
volumeValue.value = percent;
lastSetVolume.current = percent;
}
}
const muted = await castSession.isMute();
if (muted !== isMutedRef.current) {
isMutedRef.current = muted;
setIsMuted(muted);
}
} catch {
// Ignore errors
}
}, 1000); // Poll less frequently
return () => clearInterval(interval);
}, [visible, castSession, volumeValue]);
// Volume change during sliding - update display only, don't call API
const handleVolumeChange = useCallback((value: number) => {
const rounded = Math.round(value);
setDisplayVolume(rounded);
}, []);
// Volume change complete - call API
const handleVolumeComplete = useCallback(
async (value: number) => {
isSliding.current = false;
const rounded = Math.round(value);
setDisplayVolume(rounded);
lastSetVolume.current = rounded;
try {
if (castSession) {
await castSession.setVolume(rounded / 100);
}
} catch (error) {
console.error("[Connection Menu] Volume error:", error);
}
},
[castSession],
);
// Toggle mute
const handleToggleMute = useCallback(async () => {
if (!castSession) return;
try {
const newMute = !isMuted;
await castSession.setMute(newMute);
isMutedRef.current = newMute;
setIsMuted(newMute);
} catch (error) {
console.error("[Connection Menu] Mute error:", error);
}
}, [castSession, isMuted]);
// Disconnect
const handleDisconnect = useCallback(async () => {
try {
if (onDisconnect) {
await onDisconnect();
}
} catch (error) {
console.error("[Connection Menu] Disconnect error:", error);
} finally {
onClose();
}
}, [onDisconnect, onClose]);
return (
<Modal
visible={visible}
transparent={true}
animationType='slide'
onRequestClose={onClose}
>
<GestureHandlerRootView style={{ flex: 1 }}>
<Pressable
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.9)",
justifyContent: "flex-end",
}}
onPress={onClose}
>
<Pressable
style={{
backgroundColor: "#1a1a1a",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: insets.bottom + 16,
}}
onPress={(e) => e.stopPropagation()}
>
{/* Header with device name */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#333",
}}
>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
>
<View
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: protocolColor,
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons name='tv' size={20} color='white' />
</View>
<View>
<Text
style={{ color: "white", fontSize: 16, fontWeight: "600" }}
>
{castDevice?.friendlyName || t("casting_player.chromecast")}
</Text>
<Text style={{ color: protocolColor, fontSize: 12 }}>
{t("casting_player.connected")}
</Text>
</View>
</View>
<Pressable onPress={onClose} style={{ padding: 8 }}>
<Ionicons name='close' size={24} color='white' />
</Pressable>
</View>
{/* Volume Control */}
<View style={{ padding: 16 }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<Text style={{ color: "#999", fontSize: 12 }}>
{t("casting_player.volume")}
</Text>
<Text style={{ color: "white", fontSize: 14 }}>
{isMuted ? t("casting_player.muted") : `${displayVolume}%`}
</Text>
</View>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
>
<Pressable
onPress={handleToggleMute}
style={{
padding: 8,
borderRadius: 20,
backgroundColor: isMuted ? protocolColor : "transparent",
}}
>
<Ionicons
name={isMuted ? "volume-mute" : "volume-low"}
size={20}
color={isMuted ? "white" : "#999"}
/>
</Pressable>
<View style={{ flex: 1 }}>
<Slider
style={{ width: "100%", height: 40 }}
progress={volumeValue}
minimumValue={minimumValue}
maximumValue={maximumValue}
theme={{
disableMinTrackTintColor: "#333",
maximumTrackTintColor: "#333",
minimumTrackTintColor: isMuted ? "#666" : protocolColor,
bubbleBackgroundColor: protocolColor,
}}
onSlidingStart={() => {
isSliding.current = true;
}}
onValueChange={async (value) => {
volumeValue.value = value;
handleVolumeChange(value);
// Unmute when adjusting volume - use ref to avoid
// stale closure and prevent repeated async calls
if (isMutedRef.current) {
isMutedRef.current = false;
setIsMuted(false);
try {
await castSession?.setMute(false);
} catch (error: unknown) {
console.error(
"[ChromecastConnectionMenu] Failed to unmute:",
error,
);
isMutedRef.current = true;
setIsMuted(true); // Rollback on failure
}
}
}}
onSlidingComplete={handleVolumeComplete}
panHitSlop={{ top: 20, bottom: 20, left: 0, right: 0 }}
/>
</View>
<Ionicons
name='volume-high'
size={20}
color={isMuted ? "#666" : "#999"}
/>
</View>
</View>
{/* Disconnect button */}
<View style={{ paddingHorizontal: 16 }}>
<Pressable
onPress={handleDisconnect}
style={{
backgroundColor: protocolColor,
padding: 14,
borderRadius: 8,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 8,
}}
>
<Ionicons name='power' size={20} color='white' />
<Text
style={{ color: "white", fontSize: 14, fontWeight: "500" }}
>
{t("casting_player.disconnect")}
</Text>
</Pressable>
</View>
</Pressable>
</Pressable>
</GestureHandlerRootView>
</Modal>
);
};

View File

@@ -1,348 +0,0 @@
/**
* Chromecast Device Info Sheet
* Shows device details, volume control, and disconnect option
*/
import { Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useCastSession } from "react-native-google-cast";
import { useSharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
interface ChromecastDeviceSheetProps {
visible: boolean;
onClose: () => void;
device: { friendlyName?: string } | null;
onDisconnect: () => Promise<void>;
volume?: number;
onVolumeChange?: (volume: number) => Promise<void>;
}
export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
visible,
onClose,
device,
onDisconnect,
volume = 0.5,
onVolumeChange,
}) => {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [displayVolume, setDisplayVolume] = useState(Math.round(volume * 100));
const volumeValue = useSharedValue(volume * 100);
const minimumValue = useSharedValue(0);
const maximumValue = useSharedValue(100);
const castSession = useCastSession();
const volumeDebounceRef = useRef<NodeJS.Timeout | null>(null);
const [isMuted, setIsMuted] = useState(false);
const isSliding = useRef(false);
const lastSetVolume = useRef(Math.round(volume * 100));
// Sync volume slider with prop changes (updates from physical buttons)
// Skip updates while user is actively sliding to avoid overwriting drag
useEffect(() => {
if (isSliding.current) return;
volumeValue.value = volume * 100;
setDisplayVolume(Math.round(volume * 100));
}, [volume, volumeValue]);
// Poll for volume and mute updates when sheet is visible to catch physical button changes
useEffect(() => {
if (!visible || !castSession) return;
// Get initial mute state
castSession
.isMute()
.then(setIsMuted)
.catch(() => {});
// Poll CastSession for device volume and mute state (only when not sliding)
const interval = setInterval(async () => {
if (isSliding.current) return;
try {
const deviceVolume = await castSession.getVolume();
if (deviceVolume !== undefined) {
const volumePercent = Math.round(deviceVolume * 100);
// Only update if external change (physical buttons)
if (Math.abs(volumePercent - lastSetVolume.current) > 2) {
setDisplayVolume(volumePercent);
volumeValue.value = volumePercent;
lastSetVolume.current = volumePercent;
}
}
// Check mute state
const muteState = await castSession.isMute();
setIsMuted(muteState);
} catch {
// Ignore errors - device might be disconnected
}
}, 1000);
return () => clearInterval(interval);
}, [visible, castSession, volumeValue]);
const handleDisconnect = async () => {
setIsDisconnecting(true);
try {
await onDisconnect();
onClose();
} catch (error) {
console.error("Failed to disconnect:", error);
} finally {
setIsDisconnecting(false);
}
};
const handleVolumeComplete = async (value: number) => {
const newVolume = value / 100;
setDisplayVolume(Math.round(value));
try {
// Use CastSession.setVolume for DEVICE volume control
// This works even when no media is playing, unlike setStreamVolume
if (castSession) {
await castSession.setVolume(newVolume);
} else if (onVolumeChange) {
// Fallback to prop method if session not available
await onVolumeChange(newVolume);
}
} catch (error) {
console.error("[Volume] Error setting volume:", error);
}
};
// Debounced volume update during sliding for smooth live feedback
const handleVolumeChange = useCallback(
(value: number) => {
setDisplayVolume(Math.round(value));
// Debounce the API call to avoid too many requests
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
volumeDebounceRef.current = setTimeout(async () => {
const newVolume = value / 100;
try {
if (castSession) {
await castSession.setVolume(newVolume);
}
} catch {
// Ignore errors during sliding
}
}, 150); // 150ms debounce
},
[castSession],
);
// Toggle mute state
const handleToggleMute = useCallback(async () => {
if (!castSession) return;
try {
const newMuteState = !isMuted;
await castSession.setMute(newMuteState);
setIsMuted(newMuteState);
} catch (error) {
console.error("[Volume] Error toggling mute:", error);
}
}, [castSession, isMuted]);
// Cleanup debounce timer on unmount
useEffect(() => {
return () => {
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
};
}, []);
return (
<Modal
visible={visible}
transparent={true}
animationType='slide'
onRequestClose={onClose}
>
<GestureHandlerRootView style={{ flex: 1 }}>
<Pressable
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.85)",
justifyContent: "flex-end",
}}
onPress={onClose}
>
<Pressable
style={{
backgroundColor: "#1a1a1a",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingBottom: insets.bottom + 16,
}}
onPress={(e) => e.stopPropagation()}
>
{/* Header */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#333",
}}
>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
>
<Ionicons name='tv' size={24} color='#a855f7' />
<Text
style={{ color: "white", fontSize: 18, fontWeight: "600" }}
>
{t("casting_player.chromecast")}
</Text>
</View>
<Pressable onPress={onClose} style={{ padding: 8 }}>
<Ionicons name='close' size={24} color='white' />
</Pressable>
</View>
{/* Device info */}
<View style={{ padding: 16 }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ color: "#999", fontSize: 12, marginBottom: 4 }}>
{t("casting_player.device_name")}
</Text>
<Text
style={{ color: "white", fontSize: 16, fontWeight: "500" }}
>
{device?.friendlyName || t("casting_player.unknown_device")}
</Text>
</View>
{/* Volume control */}
<View style={{ marginBottom: 24 }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<Text style={{ color: "#999", fontSize: 12 }}>
{t("casting_player.volume")}
</Text>
<Text style={{ color: "white", fontSize: 14 }}>
{isMuted ? t("casting_player.muted") : `${displayVolume}%`}
</Text>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 12,
}}
>
{/* Mute button */}
<Pressable
onPress={handleToggleMute}
style={{
padding: 8,
borderRadius: 20,
backgroundColor: isMuted ? "#a855f7" : "transparent",
}}
>
<Ionicons
name={isMuted ? "volume-mute" : "volume-low"}
size={20}
color={isMuted ? "white" : "#999"}
/>
</Pressable>
<View style={{ flex: 1 }}>
<Slider
style={{ width: "100%", height: 40 }}
progress={volumeValue}
minimumValue={minimumValue}
maximumValue={maximumValue}
theme={{
disableMinTrackTintColor: "#333",
maximumTrackTintColor: "#333",
minimumTrackTintColor: isMuted ? "#666" : "#a855f7",
bubbleBackgroundColor: "#a855f7",
}}
onSlidingStart={async () => {
isSliding.current = true;
// Auto-unmute when user starts adjusting volume
if (isMuted && castSession) {
setIsMuted(false);
try {
await castSession.setMute(false);
} catch (error) {
console.error("[Volume] Failed to unmute:", error);
setIsMuted(true); // Rollback on failure
}
}
}}
onValueChange={(value) => {
volumeValue.value = value;
handleVolumeChange(value);
}}
onSlidingComplete={(value) => {
isSliding.current = false;
lastSetVolume.current = Math.round(value);
handleVolumeComplete(value);
}}
panHitSlop={{ top: 20, bottom: 20, left: 0, right: 0 }}
/>
</View>
<Ionicons
name='volume-high'
size={20}
color={isMuted ? "#666" : "#999"}
/>
</View>
</View>
{/* Disconnect button */}
<Pressable
onPress={handleDisconnect}
disabled={isDisconnecting}
style={{
backgroundColor: "#a855f7",
padding: 16,
borderRadius: 8,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 8,
opacity: isDisconnecting ? 0.5 : 1,
}}
>
<Ionicons
name='power'
size={20}
color='white'
style={{ marginTop: 2 }}
/>
<Text
style={{ color: "white", fontSize: 16, fontWeight: "600" }}
>
{isDisconnecting
? t("casting_player.disconnecting")
: t("casting_player.stop_casting")}
</Text>
</Pressable>
</View>
</Pressable>
</Pressable>
</GestureHandlerRootView>
</Modal>
);
};

View File

@@ -1,356 +0,0 @@
/**
* Episode List for Chromecast Player
* Displays list of episodes for TV shows with thumbnails
*/
import { Ionicons } from "@expo/vector-icons";
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, Modal, Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { truncateTitle } from "@/utils/casting/helpers";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
interface ChromecastEpisodeListProps {
visible: boolean;
onClose: () => void;
currentItem: BaseItemDto | null;
episodes: BaseItemDto[];
onSelectEpisode: (episode: BaseItemDto) => void;
api: Api | null;
}
export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
visible,
onClose,
currentItem,
episodes,
onSelectEpisode,
api,
}) => {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const flatListRef = useRef<FlatList>(null);
const [selectedSeason, setSelectedSeason] = useState<number | null>(null);
const scrollRetryCountRef = useRef(0);
const scrollRetryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const MAX_SCROLL_RETRIES = 3;
// Cleanup pending retry timeout on unmount
useEffect(() => {
return () => {
if (scrollRetryTimeoutRef.current) {
clearTimeout(scrollRetryTimeoutRef.current);
scrollRetryTimeoutRef.current = null;
}
scrollRetryCountRef.current = 0;
};
}, []);
// Get unique seasons from episodes
const seasons = useMemo(() => {
const seasonSet = new Set<number>();
for (const ep of episodes) {
if (ep.ParentIndexNumber !== undefined && ep.ParentIndexNumber !== null) {
seasonSet.add(ep.ParentIndexNumber);
}
}
return Array.from(seasonSet).sort((a, b) => a - b);
}, [episodes]);
// Filter episodes by selected season and exclude virtual episodes
const filteredEpisodes = useMemo(() => {
let eps = episodes;
// Filter by season if selected
if (selectedSeason !== null) {
eps = eps.filter((ep) => ep.ParentIndexNumber === selectedSeason);
}
// Filter out virtual episodes (episodes without actual video files)
// LocationType === "Virtual" means the episode doesn't have a media file
eps = eps.filter((ep) => ep.LocationType !== "Virtual");
return eps;
}, [episodes, selectedSeason]);
// Set initial season to current episode's season
useEffect(() => {
if (currentItem?.ParentIndexNumber !== undefined) {
setSelectedSeason(currentItem.ParentIndexNumber);
}
}, [currentItem]);
useEffect(() => {
// Reset retry counter when visibility or data changes
scrollRetryCountRef.current = 0;
if (scrollRetryTimeoutRef.current) {
clearTimeout(scrollRetryTimeoutRef.current);
}
if (visible && currentItem && filteredEpisodes.length > 0) {
const currentIndex = filteredEpisodes.findIndex(
(ep) => ep.Id === currentItem.Id,
);
if (currentIndex !== -1 && flatListRef.current) {
// Delay to ensure FlatList is rendered
const timeoutId = setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: currentIndex,
animated: true,
viewPosition: 0.5, // Center the item
});
}, 300);
return () => {
clearTimeout(timeoutId);
if (scrollRetryTimeoutRef.current) {
clearTimeout(scrollRetryTimeoutRef.current);
}
};
}
}
}, [visible, currentItem, filteredEpisodes]);
const renderEpisode = ({ item }: { item: BaseItemDto }) => {
const isCurrentEpisode = item.Id === currentItem?.Id;
return (
<Pressable
onPress={() => {
onSelectEpisode(item);
onClose();
}}
style={{
flexDirection: "row",
padding: 12,
// Translucent (not solid) purple so the dark base shows through and
// the row's text — incl. the purple S:E label — stays readable. The
// play-circle icon also marks the current episode.
backgroundColor: isCurrentEpisode
? "rgba(168, 85, 247, 0.25)"
: "transparent",
borderRadius: 8,
marginBottom: 8,
}}
>
{/* Thumbnail */}
<View
style={{
width: 120,
height: 68,
borderRadius: 4,
overflow: "hidden",
backgroundColor: "#1a1a1a",
}}
>
{(() => {
const imageUrl =
api && item.Id ? getPrimaryImageUrl({ api, item }) : null;
if (imageUrl) {
return (
<Image
source={{ uri: imageUrl }}
style={{ width: "100%", height: "100%" }}
contentFit='cover'
/>
);
}
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Ionicons name='film-outline' size={32} color='#333' />
</View>
);
})()}
</View>
{/* Episode info */}
<View style={{ flex: 1, marginLeft: 12, justifyContent: "center" }}>
<Text
style={{
color: "white",
fontSize: 14,
fontWeight: "600",
marginBottom: 4,
}}
numberOfLines={1}
>
{item.IndexNumber != null ? `${item.IndexNumber}. ` : ""}
{truncateTitle(item.Name || t("casting_player.unknown"), 30)}
</Text>
{item.Overview && (
<Text
style={{
color: "#999",
fontSize: 12,
marginBottom: 4,
}}
numberOfLines={2}
>
{item.Overview}
</Text>
)}
<View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>
{item.ParentIndexNumber !== undefined &&
item.IndexNumber !== undefined && (
<Text
style={{ color: "#a855f7", fontSize: 11, fontWeight: "600" }}
>
S{String(item.ParentIndexNumber).padStart(2, "0")}:E
{String(item.IndexNumber).padStart(2, "0")}
</Text>
)}
{item.ProductionYear && (
<Text style={{ color: "#666", fontSize: 11 }}>
{item.ProductionYear}
</Text>
)}
{item.RunTimeTicks && (
<Text style={{ color: "#666", fontSize: 11 }}>
{Math.round(item.RunTimeTicks / 600000000)}{" "}
{t("casting_player.minutes_short")}
</Text>
)}
</View>
</View>
{isCurrentEpisode && (
<View
style={{
justifyContent: "center",
marginLeft: 8,
}}
>
<Ionicons name='play-circle' size={24} color='white' />
</View>
)}
</Pressable>
);
};
return (
<Modal
visible={visible}
transparent={true}
animationType='slide'
onRequestClose={onClose}
>
<Pressable
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.85)",
}}
onPress={onClose}
>
<Pressable
style={{
flex: 1,
paddingTop: insets.top,
}}
onPress={(e) => e.stopPropagation()}
>
{/* Header */}
<View
style={{
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#333",
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: seasons.length > 1 ? 12 : 0,
}}
>
<Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>
{t("casting_player.episodes")}
</Text>
<Pressable onPress={onClose} style={{ padding: 8 }}>
<Ionicons name='close' size={24} color='white' />
</Pressable>
</View>
{/* Season selector */}
{seasons.length > 1 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 8 }}
>
{seasons.map((season) => (
<Pressable
key={season}
onPress={() => setSelectedSeason(season)}
style={{
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor:
selectedSeason === season ? "#a855f7" : "#1a1a1a",
}}
>
<Text
style={{
color: "white",
fontSize: 14,
fontWeight: selectedSeason === season ? "600" : "400",
}}
>
{t("casting_player.season", { number: season })}
</Text>
</Pressable>
))}
</ScrollView>
)}
</View>
{/* Episode list */}
<FlatList
ref={flatListRef}
data={filteredEpisodes}
renderItem={renderEpisode}
keyExtractor={(item, index) => item.Id || `episode-${index}`}
contentContainerStyle={{
padding: 16,
paddingBottom: insets.bottom + 16,
}}
showsVerticalScrollIndicator={false}
onScrollToIndexFailed={(info) => {
// Bounded retry for scroll failures
if (
scrollRetryCountRef.current >= MAX_SCROLL_RETRIES ||
info.index >= filteredEpisodes.length
) {
return;
}
scrollRetryCountRef.current += 1;
if (scrollRetryTimeoutRef.current) {
clearTimeout(scrollRetryTimeoutRef.current);
}
scrollRetryTimeoutRef.current = setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: info.index,
animated: true,
viewPosition: 0.5,
});
}, 500);
}}
/>
</Pressable>
</Pressable>
</Modal>
);
};

View File

@@ -1,304 +0,0 @@
/**
* Chromecast Settings Menu
* Configure version, quality (bitrate cap), audio, subtitles, and playback speed.
* Every "selected" row is driven by the active CastSelection — no [0] fallbacks.
*/
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import type { AudioTrack, SubtitleTrack } from "@/utils/casting/types";
export interface VersionOption {
id: string;
name: string;
}
export interface QualityOption {
key: string;
value: number | undefined;
}
interface ChromecastSettingsMenuProps {
visible: boolean;
onClose: () => void;
versions: VersionOption[];
selectedVersionId: string;
onVersionChange: (id: string) => void;
qualities: QualityOption[];
selectedMaxBitrate: number | undefined;
onQualityChange: (value: number | undefined) => void;
audioTracks: AudioTrack[];
selectedAudioIndex: number;
onAudioChange: (index: number) => void;
subtitleTracks: SubtitleTrack[];
/** -1 = subtitles off. */
selectedSubtitleIndex: number;
onSubtitleChange: (index: number) => void;
playbackSpeed: number;
onPlaybackSpeedChange: (speed: number) => void;
}
const PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
const ACCENT = "#a855f7";
export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
visible,
onClose,
versions,
selectedVersionId,
onVersionChange,
qualities,
selectedMaxBitrate,
onQualityChange,
audioTracks,
selectedAudioIndex,
onAudioChange,
subtitleTracks,
selectedSubtitleIndex,
onSubtitleChange,
playbackSpeed,
onPlaybackSpeedChange,
}) => {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const [expandedSection, setExpandedSection] = useState<string | null>(null);
const toggleSection = (section: string) => {
setExpandedSection(expandedSection === section ? null : section);
};
const renderSectionHeader = (
title: string,
icon: keyof typeof Ionicons.glyphMap,
sectionKey: string,
) => (
<Pressable
onPress={() => toggleSection(sectionKey)}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#333",
}}
>
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<Ionicons name={icon} size={20} color='white' />
<Text style={{ color: "white", fontSize: 16, fontWeight: "500" }}>
{title}
</Text>
</View>
<Ionicons
name={expandedSection === sectionKey ? "chevron-up" : "chevron-down"}
size={20}
color='#999'
/>
</Pressable>
);
const renderRow = (
key: string | number,
label: string,
sublabel: string | null,
selected: boolean,
onPress: () => void,
) => (
<Pressable
key={key}
onPress={() => {
onPress();
setExpandedSection(null);
}}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
backgroundColor: selected ? "#2a2a2a" : "transparent",
}}
>
<View>
<Text style={{ color: "white", fontSize: 15 }}>{label}</Text>
{sublabel ? (
<Text style={{ color: "#999", fontSize: 13, marginTop: 2 }}>
{sublabel}
</Text>
) : null}
</View>
{selected ? <Ionicons name='checkmark' size={20} color={ACCENT} /> : null}
</Pressable>
);
return (
<Modal
visible={visible}
transparent={true}
animationType='slide'
onRequestClose={onClose}
>
<Pressable
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.85)",
justifyContent: "flex-end",
}}
onPress={onClose}
>
<Pressable
style={{
backgroundColor: "#1a1a1a",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: "80%",
paddingBottom: insets.bottom,
}}
onPress={(e) => e.stopPropagation()}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#333",
}}
>
<Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>
{t("casting_player.playback_settings")}
</Text>
<Pressable onPress={onClose} style={{ padding: 8 }}>
<Ionicons name='close' size={24} color='white' />
</Pressable>
</View>
<ScrollView>
{/* Version — only when the item has more than one MediaSource */}
{versions.length > 1 &&
renderSectionHeader(
t("casting_player.version"),
"albums-outline",
"version",
)}
{versions.length > 1 && expandedSection === "version" && (
<View style={{ paddingVertical: 8 }}>
{versions.map((v) =>
renderRow(
v.id,
v.name,
null,
v.id === selectedVersionId,
() => onVersionChange(v.id),
),
)}
</View>
)}
{/* Quality (bitrate cap) */}
{renderSectionHeader(
t("casting_player.quality"),
"film-outline",
"quality",
)}
{expandedSection === "quality" && (
<View style={{ paddingVertical: 8 }}>
{qualities.map((q) =>
renderRow(
q.key,
q.key,
null,
q.value === selectedMaxBitrate,
() => onQualityChange(q.value),
),
)}
</View>
)}
{/* Audio — only when more than one track */}
{audioTracks.length > 1 &&
renderSectionHeader(
t("casting_player.audio"),
"musical-notes",
"audio",
)}
{audioTracks.length > 1 && expandedSection === "audio" && (
<View style={{ paddingVertical: 8 }}>
{audioTracks.map((track) =>
renderRow(
track.index,
track.displayTitle ||
track.language ||
t("casting_player.unknown"),
track.codec ? track.codec.toUpperCase() : null,
track.index === selectedAudioIndex,
() => onAudioChange(track.index),
),
)}
</View>
)}
{/* Subtitles */}
{subtitleTracks.length > 0 &&
renderSectionHeader(
t("casting_player.subtitles"),
"text",
"subtitles",
)}
{subtitleTracks.length > 0 && expandedSection === "subtitles" && (
<View style={{ paddingVertical: 8 }}>
{renderRow(
"off",
t("casting_player.none"),
null,
selectedSubtitleIndex < 0,
() => onSubtitleChange(-1),
)}
{subtitleTracks.map((track) =>
renderRow(
track.index,
track.displayTitle ||
track.language ||
t("casting_player.unknown"),
[
track.codec ? track.codec.toUpperCase() : "",
track.isForced ? t("casting_player.forced") : "",
]
.filter(Boolean)
.join(" • ") || null,
track.index === selectedSubtitleIndex,
() => onSubtitleChange(track.index),
),
)}
</View>
)}
{/* Playback speed */}
{renderSectionHeader(
t("casting_player.playback_speed"),
"speedometer",
"speed",
)}
{expandedSection === "speed" && (
<View style={{ paddingVertical: 8 }}>
{PLAYBACK_SPEEDS.map((speed) =>
renderRow(
speed,
speed === 1 ? t("casting_player.normal") : `${speed}x`,
null,
Math.abs(playbackSpeed - speed) < 0.01,
() => onPlaybackSpeedChange(speed),
),
)}
</View>
)}
</ScrollView>
</Pressable>
</Pressable>
</Modal>
);
};

View File

@@ -1,171 +0,0 @@
/**
* Hook for managing Chromecast segments (intro, credits, recap, commercial, preview)
* Integrates with autoskip API for segment detection
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { isWithinSegment } from "@/utils/casting/helpers";
import type { ChromecastSegmentData } from "@/utils/chromecast/options";
import { useSegments } from "@/utils/segments";
export const useChromecastSegments = (
item: BaseItemDto | null,
currentProgressMs: number,
isOffline = false,
) => {
const api = useAtomValue(apiAtom);
const { settings } = useSettings();
// Fetch segments from autoskip API
const { data: segmentData } = useSegments(
item?.Id || "",
isOffline,
undefined, // downloadedFiles parameter
api,
);
// Parse segments into usable format
const segments = useMemo<ChromecastSegmentData>(() => {
if (!segmentData) {
return {
intro: null,
credits: null,
recap: null,
commercial: [],
preview: [],
};
}
const intro =
segmentData.introSegments && segmentData.introSegments.length > 0
? {
start: segmentData.introSegments[0].startTime,
end: segmentData.introSegments[0].endTime,
}
: null;
const credits =
segmentData.creditSegments && segmentData.creditSegments.length > 0
? {
start: segmentData.creditSegments[0].startTime,
end: segmentData.creditSegments[0].endTime,
}
: null;
const recap =
segmentData.recapSegments && segmentData.recapSegments.length > 0
? {
start: segmentData.recapSegments[0].startTime,
end: segmentData.recapSegments[0].endTime,
}
: null;
const commercial = (segmentData.commercialSegments || []).map((seg) => ({
start: seg.startTime,
end: seg.endTime,
}));
const preview = (segmentData.previewSegments || []).map((seg) => ({
start: seg.startTime,
end: seg.endTime,
}));
return { intro, credits, recap, commercial, preview };
}, [segmentData]);
// Check which segment we're currently in
// currentProgressMs is in milliseconds; isWithinSegment() converts ms→seconds internally
// before comparing with segment times (which are in seconds from the autoskip API)
const currentSegment = useMemo(() => {
if (isWithinSegment(currentProgressMs, segments.intro)) {
return { type: "intro" as const, segment: segments.intro };
}
if (isWithinSegment(currentProgressMs, segments.credits)) {
return { type: "credits" as const, segment: segments.credits };
}
if (isWithinSegment(currentProgressMs, segments.recap)) {
return { type: "recap" as const, segment: segments.recap };
}
for (const commercial of segments.commercial) {
if (isWithinSegment(currentProgressMs, commercial)) {
return { type: "commercial" as const, segment: commercial };
}
}
for (const preview of segments.preview) {
if (isWithinSegment(currentProgressMs, preview)) {
return { type: "preview" as const, segment: preview };
}
}
return null;
}, [currentProgressMs, segments]);
// Skip functions
const skipIntro = useCallback(
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (segments.intro) {
await seekFn(segments.intro.end * 1000);
}
},
[segments.intro],
);
const skipCredits = useCallback(
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (segments.credits) {
await seekFn(segments.credits.end * 1000);
}
},
[segments.credits],
);
const skipSegment = useCallback(
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (currentSegment?.segment) {
await seekFn(currentSegment.segment.end * 1000);
}
},
[currentSegment],
);
// Auto-skip logic based on settings
const shouldAutoSkip = useMemo(() => {
if (!currentSegment) return false;
switch (currentSegment.type) {
case "intro":
return settings?.skipIntro === "auto";
case "credits":
return settings?.skipOutro === "auto";
case "recap":
return settings?.skipRecap === "auto";
case "commercial":
return settings?.skipCommercial === "auto";
case "preview":
return settings?.skipPreview === "auto";
default:
return false;
}
}, [
currentSegment,
settings?.skipIntro,
settings?.skipOutro,
settings?.skipRecap,
settings?.skipCommercial,
settings?.skipPreview,
]);
return {
segments,
currentSegment,
skipIntro,
skipCredits,
skipSegment,
shouldAutoSkip,
hasIntro: !!segments.intro,
hasCredits: !!segments.credits,
};
};

View File

@@ -133,6 +133,7 @@ const HomeMobile = () => {
onPress={() => { onPress={() => {
router.push("/(auth)/downloads"); router.push("/(auth)/downloads");
}} }}
className='ml-1.5'
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
> >
<Feather <Feather

View File

@@ -60,7 +60,7 @@ export const ItemPeopleSections: React.FC<Props> = ({ item, ...props }) => {
return ( return (
<MoreMoviesWithActor <MoreMoviesWithActor
key={`${person.Id}-${idx}`} key={person.Id}
currentItem={item} currentItem={item}
actorId={person.Id} actorId={person.Id}
actorName={person.Name} actorName={person.Name}

View File

@@ -1,6 +1,6 @@
import { t } from "i18next"; import { t } from "i18next";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { Platform, ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
@@ -107,7 +107,7 @@ export const TVAddServerForm: React.FC<TVAddServerFormProps> = ({
</View> </View>
{/* Pair with Phone */} {/* Pair with Phone */}
{Platform.OS !== "ios" && onStartPairing && ( {onStartPairing && (
<View> <View>
<Button <Button
onPress={onStartPairing} onPress={onStartPairing}

View File

@@ -1,103 +0,0 @@
/**
* Player-agnostic "next episode" countdown card. The parent owns the timer and
* positioning — this component only renders the next episode's poster, title,
* the remaining seconds, and the Play-now / Cancel actions.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { useTranslation } from "react-i18next";
import { Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
interface AutoplayCountdownProps {
/** The episode that will play next. */
nextEpisode: BaseItemDto;
/** Poster image URL for the next episode, or null. */
posterUrl: string | null;
/** Seconds left before the next episode plays. */
secondsRemaining: number;
/** Play the next episode immediately. */
onPlayNow: () => void;
/** Cancel autoplay — the next episode will not play. */
onCancel: () => void;
}
export function AutoplayCountdown({
nextEpisode,
posterUrl,
secondsRemaining,
onPlayNow,
onCancel,
}: AutoplayCountdownProps) {
const { t } = useTranslation();
return (
<View
style={{
flexDirection: "row",
gap: 12,
width: 320,
padding: 12,
borderRadius: 12,
backgroundColor: "rgba(20, 20, 20, 0.94)",
}}
>
{posterUrl && (
<Image
source={{ uri: posterUrl }}
style={{ width: 62, height: 93, borderRadius: 6 }}
contentFit='cover'
/>
)}
<View style={{ flex: 1, justifyContent: "space-between" }}>
<View style={{ gap: 2 }}>
<Text style={{ color: "#999", fontSize: 12 }}>
{t("player.up_next")}
</Text>
<Text
style={{ color: "#fff", fontSize: 15, fontWeight: "600" }}
numberOfLines={2}
>
{nextEpisode.Name}
</Text>
<Text style={{ color: "#a855f7", fontSize: 13 }}>
{t("player.next_episode_in", { seconds: secondsRemaining })}
</Text>
</View>
<View style={{ flexDirection: "row", gap: 8, marginTop: 8 }}>
<Pressable
onPress={onPlayNow}
accessibilityRole='button'
style={{
flex: 1,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: "#a855f7",
alignItems: "center",
}}
>
<Text style={{ color: "#fff", fontWeight: "600" }}>
{t("player.play_now")}
</Text>
</Pressable>
<Pressable
onPress={onCancel}
accessibilityRole='button'
style={{
flex: 1,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: "#333",
alignItems: "center",
}}
>
<Text style={{ color: "#fff", fontWeight: "600" }}>
{t("player.cancel")}
</Text>
</Pressable>
</View>
</View>
</View>
);
}

View File

@@ -401,6 +401,10 @@ export const TVJellyseerrSearchResults: React.FC<
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const hasMovies = movieResults && movieResults.length > 0;
const hasTv = tvResults && tvResults.length > 0;
const hasPersons = personResults && personResults.length > 0;
if (loading) { if (loading) {
return null; return null;
} }
@@ -427,26 +431,22 @@ export const TVJellyseerrSearchResults: React.FC<
return ( return (
<View> <View>
{/* No section requests `hasTVPreferredFocus`: the native search field
keeps focus while typing, otherwise the first result would re-grab
focus on every keystroke as results re-render. The user navigates
down to the grid manually. */}
<TVJellyseerrMovieSection <TVJellyseerrMovieSection
title={t("search.request_movies")} title={t("search.request_movies")}
items={movieResults} items={movieResults}
isFirstSection={false} isFirstSection={hasMovies}
onItemPress={onMoviePress} onItemPress={onMoviePress}
/> />
<TVJellyseerrTvSection <TVJellyseerrTvSection
title={t("search.request_series")} title={t("search.request_series")}
items={tvResults} items={tvResults}
isFirstSection={false} isFirstSection={!hasMovies && hasTv}
onItemPress={onTvPress} onItemPress={onTvPress}
/> />
<TVJellyseerrPersonSection <TVJellyseerrPersonSection
title={t("search.actors")} title={t("search.actors")}
items={personResults} items={personResults}
isFirstSection={false} isFirstSection={!hasMovies && !hasTv && hasPersons}
onItemPress={onPersonPress} onItemPress={onPersonPress}
/> />
</View> </View>

View File

@@ -235,13 +235,10 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
module). It renders the native search bar + grid keyboard and module). It renders the native search bar + grid keyboard and
forwards typed text into the existing query pipeline via setSearch; forwards typed text into the existing query pipeline via setSearch;
our own results grid renders below. */} our own results grid renders below. */}
{/* No horizontal margin here: the native tvOS search bar centers itself
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
margins squeeze the bar's width and clip that trailing hint, so let
the native view span the full width and own its own insets. */}
<View <View
style={{ style={{
marginBottom: 24, marginBottom: 24,
marginHorizontal: HORIZONTAL_PADDING,
height: SEARCH_AREA_HEIGHT, height: SEARCH_AREA_HEIGHT,
}} }}
> >
@@ -283,17 +280,13 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
{/* Library Search Results */} {/* Library Search Results */}
{isLibraryMode && !loading && ( {isLibraryMode && !loading && (
<View style={{ gap: SECTION_GAP }}> <View style={{ gap: SECTION_GAP }}>
{sections.map((section) => ( {sections.map((section, index) => (
<TVSearchSection <TVSearchSection
key={section.key} key={section.key}
title={section.title} title={section.title}
items={section.items!} items={section.items!}
orientation={section.orientation || "vertical"} orientation={section.orientation || "vertical"}
// Never auto-focus a result. The native search field owns focus isFirstSection={index === 0}
// while typing; `hasTVPreferredFocus` here would re-grab focus on
// every keystroke as results re-render. User navigates down to the
// grid manually.
isFirstSection={false}
onItemPress={onItemPress} onItemPress={onItemPress}
onItemLongPress={onItemLongPress} onItemLongPress={onItemLongPress}
imageUrlGetter={ imageUrlGetter={

View File

@@ -297,12 +297,12 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
removeClippedSubviews={false} removeClippedSubviews={false}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
style={{ overflow: "visible" }} style={{ overflow: "visible" }}
// Edge padding via contentContainerStyle, NOT contentInset+contentOffset. contentInset={{
// contentOffset only applies on initial mount; since this FlatList is left: edgePadding,
// reused across searches (stable key), a second search left the inset right: edgePadding,
// without the offset and the grid snapped flush to the left edge. }}
contentOffset={{ x: -edgePadding, y: 0 }}
contentContainerStyle={{ contentContainerStyle={{
paddingHorizontal: edgePadding,
paddingVertical: SCALE_PADDING, paddingVertical: SCALE_PADDING,
}} }}
/> />

View File

@@ -1,62 +1,18 @@
import { Ionicons } from "@expo/vector-icons"; import { Switch, View } from "react-native";
import { useMemo } from "react";
import { View } from "react-native";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import type { ChromecastProfileMode } from "@/utils/casting/capabilities";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
import { PlatformDropdown } from "../PlatformDropdown";
const PROFILE_LABELS: Record<ChromecastProfileMode, string> = {
auto: "Automatic (recommended)",
"force-hevc": "Force HEVC / H265",
"force-h264": "Force H264",
};
export const ChromecastSettings: React.FC = ({ ...props }) => { export const ChromecastSettings: React.FC = ({ ...props }) => {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const profileOptions = useMemo(
() => [
{
options: (
["auto", "force-hevc", "force-h264"] as ChromecastProfileMode[]
).map((mode) => ({
type: "radio" as const,
label: PROFILE_LABELS[mode],
value: mode,
selected: (settings.chromecastProfile ?? "auto") === mode,
onPress: () => updateSettings({ chromecastProfile: mode }),
})),
},
],
[settings.chromecastProfile, updateSettings],
);
return ( return (
<View {...props}> <View {...props}>
<ListGroup title={"Chromecast"}> <ListGroup title={"Chromecast"}>
<ListItem <ListItem title={"Enable H265 for Chromecast"}>
title={"Profile"} <Switch
subtitle={ value={settings.enableH265ForChromecast}
"Automatic picks codecs per device. Override only if needed." onValueChange={(enableH265ForChromecast) =>
} updateSettings({ enableH265ForChromecast })
>
<PlatformDropdown
groups={profileOptions}
title={"Chromecast profile"}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{PROFILE_LABELS[settings.chromecastProfile ?? "auto"]}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
} }
/> />
</ListItem> </ListItem>

View File

@@ -196,10 +196,7 @@ export const OtherSettings: React.FC = () => {
} }
/> />
</ListItem> </ListItem>
<ListItem <ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
title={t("home.settings.other.max_auto_play_episode_count")}
disabled={pluginSettings?.maxAutoPlayEpisodeCount?.locked}
>
<PlatformDropdown <PlatformDropdown
groups={autoPlayEpisodeOptions} groups={autoPlayEpisodeOptions}
trigger={ trigger={

View File

@@ -8,7 +8,6 @@ import { BITRATES } from "@/components/BitrateSelector";
import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PlatformDropdown } from "@/components/PlatformDropdown";
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector"; import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
import DisabledSetting from "@/components/settings/DisabledSetting"; import DisabledSetting from "@/components/settings/DisabledSetting";
import useRouter from "@/hooks/useAppRouter";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
@@ -16,7 +15,6 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export const PlaybackControlsSettings: React.FC = () => { export const PlaybackControlsSettings: React.FC = () => {
const router = useRouter();
const { settings, updateSettings, pluginSettings } = useSettings(); const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -98,48 +96,6 @@ export const PlaybackControlsSettings: React.FC = () => {
[settings?.maxAutoPlayEpisodeCount?.key, t, updateSettings], [settings?.maxAutoPlayEpisodeCount?.key, t, updateSettings],
); );
// Clamp persisted value to the 5-60s bounds so the dropdown always shows a
// valid selection even if an out-of-range value was stored previously.
const autoplayCountdown = Math.min(
60,
Math.max(5, settings?.autoplayCountdownSeconds ?? 15),
);
const castAutoplayCountdown = Math.min(
60,
Math.max(5, settings?.castAutoplayCountdownSeconds ?? 30),
);
const autoplayCountdownOptions = useMemo(
() => [
{
options: AUTOPLAY_COUNTDOWN_SECONDS.map((seconds) => ({
type: "radio" as const,
label: String(seconds),
value: String(seconds),
selected: seconds === autoplayCountdown,
onPress: () => updateSettings({ autoplayCountdownSeconds: seconds }),
})),
},
],
[autoplayCountdown, updateSettings],
);
const castAutoplayCountdownOptions = useMemo(
() => [
{
options: AUTOPLAY_COUNTDOWN_SECONDS.map((seconds) => ({
type: "radio" as const,
label: String(seconds),
value: String(seconds),
selected: seconds === castAutoplayCountdown,
onPress: () =>
updateSettings({ castAutoplayCountdownSeconds: seconds }),
})),
},
],
[castAutoplayCountdown, updateSettings],
);
const playbackSpeedOptions = useMemo( const playbackSpeedOptions = useMemo(
() => [ () => [
{ {
@@ -273,10 +229,7 @@ export const PlaybackControlsSettings: React.FC = () => {
<ListItem <ListItem
title={t("home.settings.other.max_auto_play_episode_count")} title={t("home.settings.other.max_auto_play_episode_count")}
disabled={ disabled={!settings.autoPlayNextEpisode}
!settings.autoPlayNextEpisode ||
pluginSettings?.maxAutoPlayEpisodeCount?.locked
}
> >
<PlatformDropdown <PlatformDropdown
groups={autoPlayEpisodeOptions} groups={autoPlayEpisodeOptions}
@@ -295,57 +248,6 @@ export const PlaybackControlsSettings: React.FC = () => {
title={t("home.settings.other.max_auto_play_episode_count")} title={t("home.settings.other.max_auto_play_episode_count")}
/> />
</ListItem> </ListItem>
{/* Media Segment Skip Settings */}
<ListItem
title={t("home.settings.other.segment_skip_settings")}
subtitle={t("home.settings.other.segment_skip_settings_description")}
onPress={() => router.push("/settings/segment-skip/page")}
>
<Ionicons name='chevron-forward' size={20} color='#8E8D91' />
</ListItem>
<ListItem
title={t("home.settings.other.autoplay_countdown_seconds")}
disabled={!settings.autoPlayNextEpisode}
>
<PlatformDropdown
groups={autoplayCountdownOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>{autoplayCountdown}</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.autoplay_countdown_seconds")}
/>
</ListItem>
<ListItem
title={t("home.settings.other.cast_autoplay_countdown_seconds")}
disabled={!settings.autoPlayNextEpisode}
>
<PlatformDropdown
groups={castAutoplayCountdownOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{castAutoplayCountdown}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.other.cast_autoplay_countdown_seconds")}
/>
</ListItem>
</ListGroup> </ListGroup>
</DisabledSetting> </DisabledSetting>
); );
@@ -366,6 +268,3 @@ const AUTOPLAY_EPISODES_COUNT = (
{ key: "6", value: 6 }, { key: "6", value: 6 },
{ key: "7", value: 7 }, { key: "7", value: 7 },
]; ];
// Selectable next-episode countdown durations, bounded to 5-60 seconds.
const AUTOPLAY_COUNTDOWN_SECONDS: number[] = [5, 10, 15, 20, 30, 45, 60];

View File

@@ -1,10 +1,9 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { Api } from "@jellyfin/sdk";
import type { import type {
BaseItemDto, BaseItemDto,
ChapterInfo, ChapterInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { type FC, useEffect, useMemo, useRef, useState } from "react"; import { type FC, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
@@ -13,10 +12,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ChapterList } from "@/components/chapters/ChapterList"; import { ChapterList } from "@/components/chapters/ChapterList";
import { ChapterTicks } from "@/components/chapters/ChapterTicks"; import { ChapterTicks } from "@/components/chapters/ChapterTicks";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { AutoplayCountdown } from "@/components/player/AutoplayCountdown";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { chapterMarkers, chapterNameAt } from "@/utils/chapters"; import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
import SkipButton from "./SkipButton"; import SkipButton from "./SkipButton";
import { TimeDisplay } from "./TimeDisplay"; import { TimeDisplay } from "./TimeDisplay";
import { TrickplayBubble } from "./TrickplayBubble"; import { TrickplayBubble } from "./TrickplayBubble";
@@ -37,14 +35,11 @@ interface BottomControlsProps {
currentTime: number; currentTime: number;
remainingTime: number; remainingTime: number;
showSkipButton: boolean; showSkipButton: boolean;
skipButtonText: string;
showSkipCreditButton: boolean; showSkipCreditButton: boolean;
skipCreditButtonText: string;
hasContentAfterCredits: boolean; hasContentAfterCredits: boolean;
skipIntro: () => void; skipIntro: () => void;
skipCredit: () => void; skipCredit: () => void;
nextItem?: BaseItemDto | null; nextItem?: BaseItemDto | null;
api?: Api | null;
handleNextEpisodeAutoPlay: () => void; handleNextEpisodeAutoPlay: () => void;
handleNextEpisodeManual: () => void; handleNextEpisodeManual: () => void;
handleControlsInteraction: () => void; handleControlsInteraction: () => void;
@@ -95,14 +90,11 @@ export const BottomControls: FC<BottomControlsProps> = ({
currentTime, currentTime,
remainingTime, remainingTime,
showSkipButton, showSkipButton,
skipButtonText,
showSkipCreditButton, showSkipCreditButton,
skipCreditButtonText,
hasContentAfterCredits, hasContentAfterCredits,
skipIntro, skipIntro,
skipCredit, skipCredit,
nextItem, nextItem,
api,
handleNextEpisodeAutoPlay, handleNextEpisodeAutoPlay,
handleNextEpisodeManual, handleNextEpisodeManual,
handleControlsInteraction, handleControlsInteraction,
@@ -133,83 +125,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
); );
const hasChapters = chapterMarkerList.length > 1; const hasChapters = chapterMarkerList.length > 1;
// Autoplay overlay: shown under the same condition the old countdown button used.
const autoplayAllowed =
settings.autoPlayNextEpisode !== false &&
(settings.maxAutoPlayEpisodeCount.value === -1 ||
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value);
const showNextEpisodeCountdown =
autoplayAllowed &&
(!nextItem
? false
: // Show during credits if no content after, OR near end of video
(showSkipCreditButton && !hasContentAfterCredits) ||
remainingTime < 10000);
const [secondsRemaining, setSecondsRemaining] = useState(
settings.autoplayCountdownSeconds,
);
const [autoplayCancelled, setAutoplayCancelled] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Keep a stable ref to the autoplay handler so the timer effect does not
// restart when the handler identity changes.
const autoPlayHandlerRef = useRef(handleNextEpisodeAutoPlay);
autoPlayHandlerRef.current = handleNextEpisodeAutoPlay;
useEffect(() => {
if (!showNextEpisodeCountdown || autoplayCancelled) {
// Either the show-condition flipped off OR the user cancelled.
// In both cases, stop the running timer immediately so autoplay
// can't fire after Cancel was pressed.
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Only reset cancellation + seconds when the show-condition itself
// flipped off — a fresh credits/end-of-video window then starts a
// brand-new countdown. If we got here because autoplayCancelled
// just flipped true, keep it true so the countdown stays stopped.
if (!showNextEpisodeCountdown) {
setAutoplayCancelled(false);
setSecondsRemaining(settings.autoplayCountdownSeconds);
}
return;
}
setSecondsRemaining(settings.autoplayCountdownSeconds);
intervalRef.current = setInterval(() => {
setSecondsRemaining((prev) => {
if (prev <= 1) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
autoPlayHandlerRef.current();
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [
showNextEpisodeCountdown,
autoplayCancelled,
settings.autoplayCountdownSeconds,
]);
const nextEpisodePosterUrl = useMemo(
() =>
nextItem ? getPrimaryImageUrl({ api, item: nextItem, width: 200 }) : null,
[api, nextItem],
);
// Current chapter name for the always-visible header label (live playback). // Current chapter name for the always-visible header label (live playback).
const currentChapterName = useMemo( const currentChapterName = useMemo(
() => (hasChapters ? chapterNameAt(currentTime, chapters) : null), () => (hasChapters ? chapterNameAt(currentTime, chapters) : null),
@@ -287,7 +202,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
<SkipButton <SkipButton
showButton={showSkipButton} showButton={showSkipButton}
onPress={skipIntro} onPress={skipIntro}
buttonText={skipButtonText} buttonText='Skip Intro'
/> />
{/* Smart Skip Credits behavior: {/* Smart Skip Credits behavior:
- Show "Skip Credits" if there's content after credits OR no next episode - Show "Skip Credits" if there's content after credits OR no next episode
@@ -297,15 +212,22 @@ export const BottomControls: FC<BottomControlsProps> = ({
showSkipCreditButton && (hasContentAfterCredits || !nextItem) showSkipCreditButton && (hasContentAfterCredits || !nextItem)
} }
onPress={skipCredit} onPress={skipCredit}
buttonText={skipCreditButtonText} buttonText='Skip Credits'
/> />
{showNextEpisodeCountdown && !autoplayCancelled && nextItem && ( {settings.autoPlayNextEpisode !== false &&
<AutoplayCountdown (settings.maxAutoPlayEpisodeCount.value === -1 ||
nextEpisode={nextItem} settings.autoPlayEpisodeCount <
posterUrl={nextEpisodePosterUrl} settings.maxAutoPlayEpisodeCount.value) && (
secondsRemaining={secondsRemaining} <NextEpisodeCountDownButton
onPlayNow={handleNextEpisodeManual} show={
onCancel={() => setAutoplayCancelled(true)} !nextItem
? false
: // Show during credits if no content after, OR near end of video
(showSkipCreditButton && !hasContentAfterCredits) ||
remainingTime < 10000
}
onFinish={handleNextEpisodeAutoPlay}
onPress={handleNextEpisodeManual}
/> />
)} )}
</View> </View>

View File

@@ -4,15 +4,7 @@ import type {
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { import { type FC, useCallback, useEffect, useState } from "react";
type FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, useWindowDimensions, View } from "react-native"; import { StyleSheet, useWindowDimensions, View } from "react-native";
import Animated, { import Animated, {
Easing, Easing,
@@ -24,17 +16,17 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay"; import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useSegmentSkipper } from "@/hooks/useSegmentSkipper";
import { useTrickplay } from "@/hooks/useTrickplay"; import { useTrickplay } from "@/hooks/useTrickplay";
import type { TechnicalInfo } from "@/modules/mpv-player"; import type { TechnicalInfo } from "@/modules/mpv-player";
import { DownloadedItem } from "@/providers/Downloads/types"; import { DownloadedItem } from "@/providers/Downloads/types";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { useSegments } from "@/utils/segments"; import { ticksToMs } from "@/utils/time";
import { msToSeconds, ticksToMs } from "@/utils/time";
import { BottomControls } from "./BottomControls"; import { BottomControls } from "./BottomControls";
import { CenterControls } from "./CenterControls"; import { CenterControls } from "./CenterControls";
import { CONTROLS_CONSTANTS } from "./constants"; import { CONTROLS_CONSTANTS } from "./constants";
@@ -51,9 +43,6 @@ import { useControlsTimeout } from "./useControlsTimeout";
import { PlaybackSpeedScope } from "./utils/playback-speed-settings"; import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
import { type AspectRatio } from "./VideoScalingModeSelector"; import { type AspectRatio } from "./VideoScalingModeSelector";
// No-op function to avoid creating new references on every render
const noop = () => {};
interface Props { interface Props {
item: BaseItemDto; item: BaseItemDto;
isPlaying: boolean; isPlaying: boolean;
@@ -122,24 +111,6 @@ export const Controls: FC<Props> = ({
const [episodeView, setEpisodeView] = useState(false); const [episodeView, setEpisodeView] = useState(false);
const [showAudioSlider, setShowAudioSlider] = useState(false); const [showAudioSlider, setShowAudioSlider] = useState(false);
// Ref to track pending play timeout for cleanup and cancellation
const playTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Mutable ref tracking isPlaying to avoid stale closures in seekMs timeout
const playingRef = useRef(isPlaying);
useEffect(() => {
playingRef.current = isPlaying;
}, [isPlaying]);
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (playTimeoutRef.current) {
clearTimeout(playTimeoutRef.current);
}
};
}, []);
const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const { height: screenHeight, width: screenWidth } = useWindowDimensions();
const { previousItem, nextItem } = usePlaybackManager({ const { previousItem, nextItem } = usePlaybackManager({
item, item,
@@ -346,126 +317,28 @@ export const Controls: FC<Props> = ({
subtitleIndex: string; subtitleIndex: string;
}>(); }>();
// Fetch all segments for the current item const { showSkipButton, skipIntro } = useIntroSkipper(
const { data: segments } = useSegments( item.Id!,
item.Id ?? "", currentTime,
seek,
play,
offline, offline,
downloadedFiles,
api, api,
downloadedFiles,
); );
// Convert milliseconds to seconds for segment comparison const { showSkipCreditButton, skipCredit, hasContentAfterCredits } =
const currentTimeSeconds = msToSeconds(currentTime); useCreditSkipper(
const maxSeconds = maxMs ? msToSeconds(maxMs) : undefined; item.Id!,
currentTime,
// Wrapper to convert segment skip from seconds to milliseconds seek,
// Includes 200ms delay to allow seek operation to complete before resuming playback play,
const seekMs = useCallback( offline,
(timeInSeconds: number) => { api,
// Cancel any pending play call to avoid race conditions downloadedFiles,
if (playTimeoutRef.current) { maxMs,
clearTimeout(playTimeoutRef.current);
}
seek(timeInSeconds * 1000);
// Brief delay ensures the seek operation completes before resuming playback
// Without this, playback may resume from the old position
// Read latest isPlaying from ref to avoid stale closure
playTimeoutRef.current = setTimeout(() => {
if (playingRef.current) {
play();
}
playTimeoutRef.current = null;
}, 200);
},
[seek, play],
); );
// Use unified segment skipper for all segment types
const introSkipper = useSegmentSkipper({
segments: segments?.introSegments || [],
segmentType: "Intro",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const outroSkipper = useSegmentSkipper({
segments: segments?.creditSegments || [],
segmentType: "Outro",
currentTime: currentTimeSeconds,
totalDuration: maxSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const recapSkipper = useSegmentSkipper({
segments: segments?.recapSegments || [],
segmentType: "Recap",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const commercialSkipper = useSegmentSkipper({
segments: segments?.commercialSegments || [],
segmentType: "Commercial",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const previewSkipper = useSegmentSkipper({
segments: segments?.previewSegments || [],
segmentType: "Preview",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
// Determine which segment button to show (priority order)
// Commercial > Recap > Intro > Preview > Outro
const activeSegment = useMemo(() => {
if (commercialSkipper.currentSegment)
return { type: "Commercial", ...commercialSkipper };
if (recapSkipper.currentSegment) return { type: "Recap", ...recapSkipper };
if (introSkipper.currentSegment) return { type: "Intro", ...introSkipper };
if (previewSkipper.currentSegment)
return { type: "Preview", ...previewSkipper };
if (outroSkipper.currentSegment) return { type: "Outro", ...outroSkipper };
return null;
}, [
commercialSkipper.currentSegment,
recapSkipper.currentSegment,
introSkipper.currentSegment,
previewSkipper.currentSegment,
outroSkipper.currentSegment,
commercialSkipper,
recapSkipper,
introSkipper,
previewSkipper,
outroSkipper,
]);
// Legacy compatibility: map to old variable names
const showSkipButton = !!(
activeSegment &&
["Intro", "Recap", "Commercial", "Preview"].includes(activeSegment.type)
);
const skipIntro = activeSegment?.skipSegment || noop;
const showSkipCreditButton = activeSegment?.type === "Outro";
const skipCredit = outroSkipper.skipSegment || noop;
const hasContentAfterCredits =
outroSkipper.currentSegment && maxSeconds
? outroSkipper.currentSegment.endTime < maxSeconds
: false;
// Get button text based on segment type using i18n
const { t } = useTranslation();
const skipButtonText = activeSegment
? t(`player.skip_${activeSegment.type.toLowerCase()}`)
: t("player.skip_intro");
const skipCreditButtonText = t("player.skip_outro");
const goToItemCommon = useCallback( const goToItemCommon = useCallback(
(item: BaseItemDto) => { (item: BaseItemDto) => {
if (!item || !settings) { if (!item || !settings) {
@@ -691,14 +564,11 @@ export const Controls: FC<Props> = ({
currentTime={currentTime} currentTime={currentTime}
remainingTime={remainingTime} remainingTime={remainingTime}
showSkipButton={showSkipButton} showSkipButton={showSkipButton}
skipButtonText={skipButtonText}
showSkipCreditButton={showSkipCreditButton} showSkipCreditButton={showSkipCreditButton}
skipCreditButtonText={skipCreditButtonText}
hasContentAfterCredits={hasContentAfterCredits} hasContentAfterCredits={hasContentAfterCredits}
skipIntro={skipIntro} skipIntro={skipIntro}
skipCredit={skipCredit} skipCredit={skipCredit}
nextItem={nextItem} nextItem={nextItem}
api={api}
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay} handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
handleNextEpisodeManual={handleNextEpisodeManual} handleNextEpisodeManual={handleNextEpisodeManual}
handleControlsInteraction={handleControlsInteraction} handleControlsInteraction={handleControlsInteraction}

View File

@@ -1254,7 +1254,7 @@ export const Controls: FC<Props> = ({
<Text <Text
style={[styles.endsAtText, { fontSize: typography.callout }]} style={[styles.endsAtText, { fontSize: typography.callout }]}
> >
{t("player.ends_at", { time: getFinishTime() })} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
)} )}
@@ -1448,7 +1448,7 @@ export const Controls: FC<Props> = ({
<Text <Text
style={[styles.endsAtText, { fontSize: typography.callout }]} style={[styles.endsAtText, { fontSize: typography.callout }]}
> >
{t("player.ends_at", { time: getFinishTime() })} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
)} )}

View File

@@ -0,0 +1,96 @@
import type React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
TouchableOpacity,
type TouchableOpacityProps,
View,
} from "react-native";
import Animated, {
cancelAnimation,
Easing,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors";
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
onFinish?: () => void;
onPress?: () => void;
show: boolean;
}
const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
onFinish,
onPress,
show,
...props
}) => {
const progress = useSharedValue(0);
useEffect(() => {
if (show) {
progress.value = 0;
progress.value = withTiming(
1,
{
duration: 10000, // 10 seconds
easing: Easing.linear,
},
(finished) => {
if (finished && onFinish) {
runOnJS(onFinish)();
}
},
);
// Cancel animation on unmount to prevent onFinish from firing after exit
return () => {
cancelAnimation(progress);
};
}
}, [show, onFinish]);
const animatedStyle = useAnimatedStyle(() => {
return {
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: `${progress.value * 100}%`,
backgroundColor: Colors.primary,
};
});
const handlePress = () => {
if (onPress) {
onPress();
}
};
const { t } = useTranslation();
if (!show) {
return null;
}
return (
<TouchableOpacity
className='w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900'
{...props}
onPress={handlePress}
>
<Animated.View style={animatedStyle} />
<View className='px-3 py-3'>
<Text numberOfLines={1} className='text-center font-bold'>
{t("player.next_episode")}
</Text>
</View>
</TouchableOpacity>
);
};
export default NextEpisodeCountDownButton;

View File

@@ -1,39 +0,0 @@
# Chromecast Cast Test Matrix
Manual verification for the device-profile work. Run each row by casting the
matching media from the app to a physical Chromecast and recording the result.
**Test device:** ___________________ (model name as reported by the app)
**App build / commit:** ___________________
**Date:** ___________________
## How to run
1. Pick a library item matching the row's codec / audio / container.
2. Cast it. Note whether it direct-plays or transcodes (server logs show
`Video is being transcoded` vs `Video is being direct played`).
3. Record the load result: OK / 2100 / infinite-loading / other.
## Matrix
| # | Video codec | Audio | Container | Approx bitrate | Direct/Transcode | Result | Notes |
|---|---|---|---|---|---|---|---|
| 1 | H.264 1080p | AAC stereo | MP4 | ~4 Mb/s | | | |
| 2 | H.264 1080p | AAC stereo | MKV | ~8 Mb/s | | | |
| 3 | H.264 1080p | AAC 5.1 | MKV | ~8 Mb/s | | | |
| 4 | H.264 1080p | AC3 5.1 | MKV | ~10 Mb/s | | | |
| 5 | H.264 1080p | DTS 5.1 | MKV | ~12 Mb/s | | | |
| 6 | H.264 1080p | TrueHD 7.1 | MKV | ~20 Mb/s | | | |
| 7 | H.264 1080p | AAC stereo | MP4 | ~16 Mb/s | | | |
| 8 | H.264 1080p | AAC stereo | MP4 | source-max | | | |
| 9 | HEVC 8-bit | AAC stereo | MKV | ~10 Mb/s | | | |
| 10 | HEVC 10-bit | AAC stereo | MKV | ~15 Mb/s | | | |
## Outcome
- Highest video bitrate that loads reliably on the test device: ___________
-> update `CONSERVATIVE_CAPABILITIES.maxVideoBitrate` in
`utils/casting/capabilities.ts` accordingly.
- Confirmed cause of issue #1423 (<= 2 Mb/s): ___________
- Confirmed cause of the 5.1 crash (#1085): ___________
- Cases where downgrade-on-failure retry rescued playback: ___________

View File

@@ -1,431 +0,0 @@
/**
* Cast autoplay watcher.
*
* Always-mounted hook: subscribes to the Chromecast `mediaStatus`, captures the
* currently-playing episode while playback is active, and on either
* (a) playback entering the Outro segment (when `skipOutro !== "auto"`), or
* (b) `IDLE + FINISHED` (hard end of media),
* starts a cancellable countdown via `castAutoplayAtom` and ultimately loads
* the next episode on the cast.
*
* The countdown atom is driven here; the casting-player overlay reads it.
* Cancellation (overlay's Cancel button) sets the atom to `null` externally;
* the watcher reacts by clearing its interval and refusing to retrigger for
* the same item.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtom, useAtomValue } from "jotai";
import { useEffect, useRef, useState } from "react";
import {
MediaPlayerIdleReason,
MediaPlayerState,
useCastDevice,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import { toast } from "sonner-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { castAutoplayAtom } from "@/utils/atoms/castAutoplay";
import { useSettings } from "@/utils/atoms/settings";
import { loadCastMedia } from "@/utils/casting/castLoad";
import { fetchSeriesEpisodes, findNextEpisode } from "@/utils/casting/episodes";
import { useSegments } from "@/utils/segments";
/**
* Cached next-episode resolution, keyed by the captured (seriesId, currentEpisodeId)
* pair so the network calls are not repeated on every `mediaStatus` tick.
*/
interface NextEpisodeCache {
seriesId: string;
currentEpisodeId: string;
nextEpisode: BaseItemDto | null;
}
export interface ShouldStartCountdownParams {
playerState: MediaPlayerState | undefined;
idleReason: MediaPlayerIdleReason | undefined;
currentPositionMs: number;
outroStartMs: number | null;
outroEndMs: number | null;
skipOutro: string;
alreadyTriggered: boolean;
}
/**
* Pure decision helper: should the countdown start *right now*?
* Exported for testability.
*/
export const shouldStartCountdown = ({
playerState,
idleReason,
currentPositionMs,
outroStartMs,
outroEndMs,
skipOutro,
alreadyTriggered,
}: ShouldStartCountdownParams): boolean => {
if (alreadyTriggered) return false;
// (b) hard end of media — fires regardless of segment availability.
if (
playerState === MediaPlayerState.IDLE &&
idleReason === MediaPlayerIdleReason.FINISHED
) {
return true;
}
// (a) playback inside Outro segment, and Outro is not already auto-skipped.
if (
skipOutro !== "auto" &&
outroStartMs != null &&
outroEndMs != null &&
currentPositionMs >= outroStartMs &&
currentPositionMs < outroEndMs
) {
return true;
}
return false;
};
export const useCastAutoplay = (): void => {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const { settings, updateSettings } = useSettings();
const mediaStatus = useMediaStatus();
const remoteMediaClient = useRemoteMediaClient();
const castDevice = useCastDevice();
const [autoplayState, setAutoplayState] = useAtom(castAutoplayAtom);
// Continuously captured currently-playing item (full BaseItemDto, fetched
// from Jellyfin). `mediaInfo` clears at IDLE so we must capture as it plays.
const capturedItemRef = useRef<BaseItemDto | null>(null);
const capturedItemIdRef = useRef<string | null>(null);
// State mirror of the captured item id so downstream effects/hooks re-run
// *after* the async getItem resolves — depending on `contentId` directly
// would fire them before the ref is populated and they'd read stale data.
const [capturedItemId, setCapturedItemId] = useState<string | null>(null);
// Cached next-episode resolution per (seriesId, currentEpisodeId).
const nextEpisodeCacheRef = useRef<NextEpisodeCache | null>(null);
// Last item id we triggered a countdown for. Reset when captured item changes
// so the same finished episode does not retrigger.
const triggeredForItemIdRef = useRef<string | null>(null);
// Countdown interval handle.
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Track whether the atom transitioned to null while a countdown is running —
// that means the overlay cancelled, so we must not retrigger for this item.
const autoplayStateRef = useRef(autoplayState);
autoplayStateRef.current = autoplayState;
// Latest settings snapshot reachable from the interval / load callback
// without re-creating the interval on every settings change.
const settingsRef = useRef(settings);
settingsRef.current = settings;
const updateSettingsRef = useRef(updateSettings);
updateSettingsRef.current = updateSettings;
const apiRef = useRef(api);
apiRef.current = api;
const userRef = useRef(user);
userRef.current = user;
const remoteMediaClientRef = useRef(remoteMediaClient);
remoteMediaClientRef.current = remoteMediaClient;
const castDeviceRef = useRef(castDevice);
castDeviceRef.current = castDevice;
const contentId = mediaStatus?.mediaInfo?.contentId ?? null;
// --- 1. Capture the currently-playing item, full BaseItemDto. ---
useEffect(() => {
if (!contentId || !api || !user?.Id) {
// No active content: clear all captured state so downstream effects /
// useSegments stop using a stale previous-item id.
capturedItemRef.current = null;
capturedItemIdRef.current = null;
setCapturedItemId(null);
return;
}
// If the captured id changed, reset the trigger guard immediately — the
// user moved to another episode, and that new episode should be eligible.
if (capturedItemIdRef.current !== contentId) {
triggeredForItemIdRef.current = null;
}
let cancelled = false;
const controller = new AbortController();
(async () => {
try {
const res = await getUserLibraryApi(api).getItem(
{ itemId: contentId, userId: user.Id! },
{ signal: controller.signal },
);
if (cancelled) return;
capturedItemRef.current = res.data;
capturedItemIdRef.current = contentId;
// Publish the captured id as state *after* the ref is set, so the
// next-episode-resolve effect (keyed on this state) sees a populated
// ref by the time it runs.
setCapturedItemId(contentId);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError")
return;
// Non-fatal: keep whatever we last captured.
console.error("[useCastAutoplay] Failed to fetch item:", error);
}
})();
return () => {
cancelled = true;
controller.abort();
};
}, [contentId, api, user?.Id]);
// --- 2. Resolve next episode (cached per series+episode). ---
// This effect runs whenever the captured item id changes; the cache key
// prevents refetching on every mediaStatus tick.
useEffect(() => {
const item = capturedItemRef.current;
if (!item || !api || !user) return;
if (item.Type !== "Episode") {
nextEpisodeCacheRef.current = null;
return;
}
const seriesId = item.SeriesId;
const currentEpisodeId = item.Id;
if (!seriesId || !currentEpisodeId) {
nextEpisodeCacheRef.current = null;
return;
}
const cached = nextEpisodeCacheRef.current;
if (
cached &&
cached.seriesId === seriesId &&
cached.currentEpisodeId === currentEpisodeId
) {
return;
}
let cancelled = false;
(async () => {
try {
const episodes = await fetchSeriesEpisodes(api, user, seriesId);
if (cancelled) return;
nextEpisodeCacheRef.current = {
seriesId,
currentEpisodeId,
nextEpisode: findNextEpisode(episodes, currentEpisodeId),
};
} catch (error) {
console.error(
"[useCastAutoplay] Failed to resolve next episode:",
error,
);
}
})();
return () => {
cancelled = true;
};
// Depend on the *state* mirror of the captured id rather than `contentId`
// directly: `contentId` flips synchronously on the new episode, but
// `capturedItemRef.current` is only populated after the async getItem
// resolves. Keying on `capturedItemId` (set right after the ref write)
// guarantees the ref points at the new item by the time we read it here.
}, [capturedItemId, api, user]);
// --- 3. Media segments for the captured item (Outro). ---
// Matches `useChromecastSegments`: cast playback is online, no downloaded
// files context to thread through.
const { data: segmentData } = useSegments(
capturedItemId ?? "",
false,
undefined,
api,
);
const outroSegment = segmentData?.creditSegments?.[0] ?? null;
const outroStartMs = outroSegment ? outroSegment.startTime * 1000 : null;
const outroEndMs = outroSegment ? outroSegment.endTime * 1000 : null;
// --- 4. Trigger detection. ---
useEffect(() => {
// Master gate: setting must allow autoplay, and a countdown must not be
// already running. The atom drives the countdown; an active atom means
// we already triggered (possibly via overlay's Play now).
if (!settings.autoPlayNextEpisode) return;
if (autoplayState !== null) return;
const maxValue = settings.maxAutoPlayEpisodeCount.value;
if (maxValue !== -1 && settings.autoPlayEpisodeCount >= maxValue) return;
const capturedItem = capturedItemRef.current;
const capturedItemId = capturedItemIdRef.current;
if (!capturedItem || !capturedItemId) return;
if (capturedItem.Type !== "Episode") return;
const cached = nextEpisodeCacheRef.current;
if (
!cached ||
cached.currentEpisodeId !== capturedItemId ||
!cached.nextEpisode
) {
return;
}
const nextEpisode = cached.nextEpisode;
const currentPositionMs = (mediaStatus?.streamPosition ?? 0) * 1000;
const should = shouldStartCountdown({
playerState: mediaStatus?.playerState as MediaPlayerState | undefined,
idleReason: mediaStatus?.idleReason as MediaPlayerIdleReason | undefined,
currentPositionMs,
outroStartMs,
outroEndMs,
skipOutro: settings.skipOutro,
alreadyTriggered: triggeredForItemIdRef.current === capturedItemId,
});
if (!should) return;
triggeredForItemIdRef.current = capturedItemId;
setAutoplayState({
nextEpisode,
secondsRemaining: settings.castAutoplayCountdownSeconds,
});
// The countdown interval is started by the effect below (reacts to the
// atom transitioning to non-null), so this effect stays pure-decide.
}, [
mediaStatus?.playerState,
mediaStatus?.idleReason,
mediaStatus?.streamPosition,
outroStartMs,
outroEndMs,
settings.autoPlayNextEpisode,
settings.autoPlayEpisodeCount,
settings.maxAutoPlayEpisodeCount,
settings.castAutoplayCountdownSeconds,
settings.skipOutro,
autoplayState,
setAutoplayState,
]);
// --- 5. Run countdown interval whenever atom is non-null. ---
// Starting/stopping is driven by the atom value, so an external Cancel
// (overlay) that sets the atom to null naturally tears the interval down.
useEffect(() => {
if (autoplayState === null) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return;
}
// Only start an interval if one is not already running.
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
// Read latest atom value from ref to decide what to do next.
const current = autoplayStateRef.current;
if (current === null) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return;
}
const next = current.secondsRemaining - 1;
if (next > 0) {
setAutoplayState({ ...current, secondsRemaining: next });
return;
}
// Time's up — load the next episode and clear.
// Snapshot what we need; clear the interval and atom synchronously to
// avoid double-fire.
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
const episodeToLoad = current.nextEpisode;
setAutoplayState(null);
const apiLocal = apiRef.current;
const userLocal = userRef.current;
const clientLocal = remoteMediaClientRef.current;
const deviceLocal = castDeviceRef.current;
const settingsLocal = settingsRef.current;
if (!apiLocal || !userLocal?.Id || !clientLocal || !episodeToLoad?.Id) {
return;
}
// Mirror `useCastEpisodes.loadEpisode` exactly — same arguments,
// same start-position derivation.
(async () => {
try {
const startPositionMs =
(episodeToLoad.UserData?.PlaybackPositionTicks ?? 0) / 10000;
const result = await loadCastMedia({
client: clientLocal,
device: deviceLocal,
api: apiLocal,
item: episodeToLoad,
userId: userLocal.Id!,
profileMode: settingsLocal.chromecastProfile,
maxBitrateSetting: settingsLocal.chromecastMaxBitrate,
options: { startPositionMs },
});
if (!result.ok) {
console.error(
"[useCastAutoplay] Failed to load next episode:",
result.error,
);
return;
}
// Read the freshest count at the moment of the write — the
// overlay's "Play now" can reset this to 0 in parallel, and using
// a snapshot taken before the await would clobber that reset.
updateSettingsRef.current({
autoPlayEpisodeCount: settingsRef.current.autoPlayEpisodeCount + 1,
});
toast("Playing next episode");
} catch (error) {
console.error(
"[useCastAutoplay] Failed to load next episode:",
error,
);
}
})();
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [autoplayState, setAutoplayState]);
// --- 6. Final unmount cleanup is covered by the interval effect's
// return; nothing else to do here.
};
export default useCastAutoplay;

View File

@@ -1,69 +0,0 @@
import type { ImperativeRouter } from "expo-router";
import { useCallback } from "react";
import { Gesture } from "react-native-gesture-handler";
import {
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
interface UseCastDismissGestureParams {
router: ImperativeRouter;
}
/**
* Swipe-down-to-dismiss gesture cluster for the casting player modal.
* Owns the `translateY`/`context` shared values, the pan gesture, the animated
* style, and the `dismissModal` callback (also invoked by the header button).
*/
export function useCastDismissGesture({ router }: UseCastDismissGestureParams) {
// Swipe down to dismiss gesture
const translateY = useSharedValue(0);
const context = useSharedValue({ y: 0 });
const dismissModal = useCallback(() => {
// Navigate immediately without animation to prevent crashes
if (router.canGoBack()) {
router.back();
} else {
router.replace("/(auth)/(tabs)/(home)/");
}
}, [router]);
const panGesture = Gesture.Pan()
.onStart(() => {
context.value = { y: translateY.value };
})
.onUpdate((event) => {
// Only allow downward swipes from top of screen
if (event.translationY > 0) {
translateY.value = context.value.y + event.translationY;
}
})
.onEnd((event) => {
// Dismiss if swiped down more than 150px or fast swipe
if (event.translationY > 150 || event.velocityY > 600) {
// Animate down and dismiss
translateY.value = withSpring(
1000,
{
damping: 20,
stiffness: 90,
},
() => {
runOnJS(dismissModal)();
},
);
} else {
// Spring back to position
translateY.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
return { panGesture, animatedStyle, dismissModal };
}

View File

@@ -1,156 +0,0 @@
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto, UserDto } from "@jellyfin/sdk/lib/generated-client";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useCallback, useEffect, useState } from "react";
import type { Device, RemoteMediaClient } from "react-native-google-cast";
import type { Settings } from "@/utils/atoms/settings";
import { loadCastMedia } from "@/utils/casting/castLoad";
import { fetchSeriesEpisodes, findNextEpisode } from "@/utils/casting/episodes";
interface UseCastEpisodesParams {
api: Api | null;
user: UserDto | null;
currentItem: BaseItemDto | null;
remoteMediaClient: RemoteMediaClient | null;
castDevice: Device | null;
settings: Settings;
}
interface UseCastEpisodesResult {
episodes: BaseItemDto[];
nextEpisode: BaseItemDto | null;
seasonData: BaseItemDto | null;
loadEpisode: (episode: BaseItemDto) => Promise<void>;
/**
* Id of the episode currently being loaded onto the cast device, or null
* when no load is pending. The cast `customData` (and thus `currentItem`)
* lags behind the load, so consumers use this to detect the stale window
* between a `loadEpisode` call and the cast reporting the new episode.
*/
loadingEpisodeId: string | null;
}
export function useCastEpisodes({
api,
user,
currentItem,
remoteMediaClient,
castDevice,
settings,
}: UseCastEpisodesParams): UseCastEpisodesResult {
const [episodes, setEpisodes] = useState<BaseItemDto[]>([]);
const [nextEpisode, setNextEpisode] = useState<BaseItemDto | null>(null);
const [seasonData, setSeasonData] = useState<BaseItemDto | null>(null);
// Target episode id while a load is in flight; cleared once it resolves.
const [loadingEpisodeId, setLoadingEpisodeId] = useState<string | null>(null);
// Load a different episode on the Chromecast
const loadEpisode = useCallback(
async (episode: BaseItemDto) => {
if (!api || !user?.Id || !episode.Id || !remoteMediaClient) return;
setLoadingEpisodeId(episode.Id);
try {
const startPositionMs =
(episode.UserData?.PlaybackPositionTicks ?? 0) / 10000;
const result = await loadCastMedia({
client: remoteMediaClient,
device: castDevice,
api,
item: episode,
userId: user.Id,
profileMode: settings.chromecastProfile,
maxBitrateSetting: settings.chromecastMaxBitrate,
options: { startPositionMs },
});
if (!result.ok) {
console.error(
"[Casting Player] Failed to load episode:",
result.error,
);
return;
}
} catch (error) {
console.error("[Casting Player] Failed to load episode:", error);
} finally {
// Clear regardless of outcome: on success `currentItem` catches up via
// customData; on failure the stale guard must not stay stuck.
setLoadingEpisodeId(null);
}
},
[
api,
user?.Id,
remoteMediaClient,
castDevice,
settings.chromecastProfile,
settings.chromecastMaxBitrate,
],
);
// Fetch season data for season poster
useEffect(() => {
if (
currentItem?.Type !== "Episode" ||
!currentItem.SeasonId ||
!api ||
!user?.Id
)
return;
const fetchSeasonData = async () => {
try {
const userLibraryApi = getUserLibraryApi(api);
const response = await userLibraryApi.getItem({
itemId: currentItem.SeasonId!,
userId: user.Id!,
});
setSeasonData(response.data);
} catch (error) {
console.error("[Casting Player] Failed to fetch season data:", error);
setSeasonData(null);
}
};
fetchSeasonData();
}, [currentItem?.Type, currentItem?.SeasonId, api, user?.Id]);
// Fetch episodes for TV shows
useEffect(() => {
if (
currentItem?.Type !== "Episode" ||
!currentItem.SeriesId ||
!api ||
!user
)
return;
const fetchEpisodes = async () => {
try {
// Fetch ALL episodes from ALL seasons (no season filter).
const episodeList = await fetchSeriesEpisodes(
api,
user,
currentItem.SeriesId!,
);
setEpisodes(episodeList);
setNextEpisode(findNextEpisode(episodeList, currentItem.Id));
} catch (error) {
console.error("Failed to fetch episodes:", error);
}
};
fetchEpisodes();
}, [
currentItem?.Type,
currentItem?.SeriesId,
currentItem?.SeasonId,
currentItem?.Id,
api,
user,
]);
return { episodes, nextEpisode, seasonData, loadEpisode, loadingEpisodeId };
}

View File

@@ -1,94 +0,0 @@
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto, UserDto } from "@jellyfin/sdk/lib/generated-client";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useEffect, useMemo, useState } from "react";
import type { MediaStatus } from "react-native-google-cast";
interface UseCastPlayerItemParams {
api: Api | null;
user: UserDto | null;
mediaStatus: MediaStatus | null;
}
interface UseCastPlayerItemResult {
fetchedItem: BaseItemDto | null;
currentItem: BaseItemDto | null;
}
export function useCastPlayerItem({
api,
user,
mediaStatus,
}: UseCastPlayerItemParams): UseCastPlayerItemResult {
// Fetch full item data from Jellyfin by ID
const [fetchedItem, setFetchedItem] = useState<BaseItemDto | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchItemData = async () => {
const itemId = mediaStatus?.mediaInfo?.contentId;
if (!itemId || !api || !user?.Id) return;
try {
const res = await getUserLibraryApi(api).getItem(
{ itemId, userId: user.Id },
{ signal: controller.signal },
);
if (!controller.signal.aborted) {
setFetchedItem(res.data);
}
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError")
return;
console.error("[Casting Player] Failed to fetch item:", error);
}
};
fetchItemData();
return () => controller.abort();
}, [mediaStatus?.mediaInfo?.contentId, api, user?.Id]);
// Extract item from customData, or use fetched item, or create a minimal fallback
const currentItem = useMemo(() => {
// Priority 1: Use fetched item from API (most reliable)
if (fetchedItem) {
return fetchedItem;
}
// Priority 2: Try customData from mediaStatus
const customData = mediaStatus?.mediaInfo?.customData as BaseItemDto | null;
if (
customData?.Type &&
(customData.ImageTags || customData.MediaSources || customData.Id)
) {
// Use customData if it has a real Type AND meaningful metadata
// (rules out placeholder objects that lack image tags, media sources, or an ID)
return customData;
}
// Priority 3: Create minimal fallback while loading
if (mediaStatus?.mediaInfo) {
const { contentId, metadata } = mediaStatus.mediaInfo;
// Derive type from metadata if available, otherwise omit to avoid
// misrepresenting episodes as movies
let metadataType: string | undefined;
if (metadata?.type === "movie") {
metadataType = "Movie";
} else if (metadata?.type === "tvShow") {
metadataType = "Episode";
}
return {
Id: contentId,
Name: metadata?.title || "Unknown",
...(metadataType ? { Type: metadataType } : {}),
ServerId: "",
} as BaseItemDto;
}
return null;
}, [fetchedItem, mediaStatus?.mediaInfo]);
return { fetchedItem, currentItem };
}

View File

@@ -1,148 +0,0 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { type RefObject, useEffect, useRef, useState } from "react";
import { MediaPlayerState, type MediaStatus } from "react-native-google-cast";
import { type SharedValue, useSharedValue } from "react-native-reanimated";
import { useTrickplay } from "@/hooks/useTrickplay";
interface TrickplayTime {
hours: number;
minutes: number;
seconds: number;
}
interface UseCastPlayerProgressParams {
/** Raw Chromecast media status, or null when no session. */
mediaStatus: MediaStatus | null;
/** Full item fetched from Jellyfin, used to derive trickplay data. */
fetchedItem: BaseItemDto | null;
/** Total media duration, in seconds. */
duration: number;
}
type TrickplayReturn = ReturnType<typeof useTrickplay>;
interface UseCastPlayerProgressResult {
/** Shared value tracking the slider progress, in milliseconds. */
sliderProgress: SharedValue<number>;
/** Shared value for the slider minimum, in milliseconds. */
sliderMin: SharedValue<number>;
/** Shared value for the slider maximum, in milliseconds. */
sliderMax: SharedValue<number>;
/** Mutable ref flag set true while the user is scrubbing. */
isScrubbing: RefObject<boolean>;
/** Trickplay time display state for the bubble. */
trickplayTime: TrickplayTime;
/** Updates the trickplay time display state. */
setTrickplayTime: (time: TrickplayTime) => void;
/** Current playback progress, in seconds (live-updating). */
progress: number;
/** Last stable playback position (seconds), for resuming across reloads. */
resumePositionRef: RefObject<number>;
/** Current trickplay image URL/coordinates, or null. */
trickPlayUrl: TrickplayReturn["trickPlayUrl"];
/** Computes the trickplay URL for a given progress in ticks. */
calculateTrickplayUrl: TrickplayReturn["calculateTrickplayUrl"];
/** Parsed trickplay metadata, or null. */
trickplayInfo: TrickplayReturn["trickplayInfo"];
}
/**
* Progress/slider/trickplay cluster for the casting player.
* Owns the slider shared values, scrub state, live-progress interpolation,
* resume-position tracking, and trickplay preview.
*/
export function useCastPlayerProgress({
mediaStatus,
fetchedItem,
duration,
}: UseCastPlayerProgressParams): UseCastPlayerProgressResult {
// Shared values for progress slider (must be initialized before any early returns)
const sliderProgress = useSharedValue(0);
const sliderMin = useSharedValue(0);
const sliderMax = useSharedValue(100);
const isScrubbing = useRef(false);
// Trickplay time display
const [trickplayTime, setTrickplayTime] = useState({
hours: 0,
minutes: 0,
seconds: 0,
});
// Live progress tracking - update every second
const [liveProgress, setLiveProgress] = useState(0);
const lastSyncPositionRef = useRef(0);
const lastSyncTimestampRef = useRef(Date.now());
// Last stable playback position (seconds), for resuming across reloads.
const resumePositionRef = useRef(0);
useEffect(() => {
// Sync refs whenever mediaStatus provides a new position
if (mediaStatus?.streamPosition !== undefined) {
lastSyncPositionRef.current = mediaStatus.streamPosition;
lastSyncTimestampRef.current = Date.now();
setLiveProgress(mediaStatus.streamPosition);
}
// Update every second when playing, deriving from last sync point
const interval = setInterval(() => {
if (
mediaStatus?.playerState === MediaPlayerState.PLAYING &&
mediaStatus?.streamPosition !== undefined
) {
const elapsed = (Date.now() - lastSyncTimestampRef.current) / 1000;
setLiveProgress(lastSyncPositionRef.current + elapsed);
} else if (mediaStatus?.streamPosition !== undefined) {
// Sync with actual position when paused/buffering
setLiveProgress(mediaStatus.streamPosition);
}
}, 1000);
return () => clearInterval(interval);
}, [mediaStatus?.playerState, mediaStatus?.streamPosition]);
// Track the last stable position so a reload mid-switch resumes correctly.
useEffect(() => {
const pos = mediaStatus?.streamPosition ?? 0;
if (mediaStatus?.playerState === MediaPlayerState.PLAYING && pos > 0) {
resumePositionRef.current = pos;
}
}, [mediaStatus?.streamPosition, mediaStatus?.playerState]);
// Derive state from raw Chromecast hooks
const progress = liveProgress; // Use live-updating progress
// Trickplay for seeking preview - use fetched item with full data
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
fetchedItem ?? null,
);
// Update slider max when duration changes
useEffect(() => {
if (duration > 0) {
sliderMax.value = duration * 1000; // Convert to milliseconds
}
}, [duration, sliderMax]);
// Update slider progress when not scrubbing
useEffect(() => {
if (!isScrubbing.current && progress > 0) {
sliderProgress.value = progress * 1000; // Convert to milliseconds
}
}, [progress, sliderProgress]);
return {
sliderProgress,
sliderMin,
sliderMax,
isScrubbing,
trickplayTime,
setTrickplayTime,
progress,
resumePositionRef,
trickPlayUrl,
calculateTrickplayUrl,
trickplayInfo,
};
}

View File

@@ -1,75 +0,0 @@
/**
* Source of truth for the active cast track / quality / version selection.
*
* Truth = the CastSelection echoed back in the cast media customData. A local
* `pending` selection is shown optimistically while a reload re-transcodes, then
* cleared once the cast reports it (reconciled) or the reload fails.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback, useEffect, useState } from "react";
import type { MediaStatus } from "react-native-google-cast";
import { resolveSelection, selectionsEqual } from "@/utils/casting/selection";
import type { CastSelection } from "@/utils/casting/types";
interface UseCastSelectionParams {
currentItem: BaseItemDto | null;
mediaStatus: MediaStatus | null | undefined;
/** Reload the cast stream with the given selection. Resolves true on success. */
reload: (selection: CastSelection) => Promise<boolean>;
}
interface UseCastSelectionResult {
/** Effective selection: optimistic pending, else cast truth, else default. */
currentSelection: CastSelection | null;
/** Merge a partial selection, show it optimistically, and reload the stream. */
applySelection: (partial: Partial<CastSelection>) => void;
}
export const useCastSelection = ({
currentItem,
mediaStatus,
reload,
}: UseCastSelectionParams): UseCastSelectionResult => {
const [pending, setPending] = useState<CastSelection | null>(null);
// Truth: the selection the cast reports as loaded, via customData.
const truth =
(
mediaStatus?.mediaInfo?.customData as
| { selection?: CastSelection }
| undefined
)?.selection ?? null;
const currentSelection: CastSelection | null =
pending ??
truth ??
(currentItem ? resolveSelection(currentItem, {}) : null);
// A new media item invalidates any pending selection from the previous one.
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on item id only
useEffect(() => {
setPending(null);
}, [currentItem?.Id]);
// Reconcile: once the cast reports the pending selection as loaded, clear it.
useEffect(() => {
if (pending && truth && selectionsEqual(pending, truth)) {
setPending(null);
}
}, [pending, truth]);
const applySelection = useCallback(
(partial: Partial<CastSelection>) => {
if (!currentSelection) return;
const next: CastSelection = { ...currentSelection, ...partial };
setPending(next);
reload(next).then((ok) => {
if (!ok) setPending(null);
});
},
[currentSelection, reload],
);
return { currentSelection, applySelection };
};

View File

@@ -1,407 +0,0 @@
/**
* Unified Casting Hook
* Protocol-agnostic casting interface - currently supports Chromecast
* Architecture allows for future protocol integrations
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useRef, useState } from "react";
import {
CastState,
useCastDevice,
useCastState,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { CastPlayerState, CastProtocol } from "@/utils/casting/types";
import { DEFAULT_CAST_STATE } from "@/utils/casting/types";
/**
* Unified hook for managing casting
* Extensible architecture supporting multiple protocols
*/
export const useCasting = (item: BaseItemDto | null) => {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
// Chromecast hooks
const client = useRemoteMediaClient();
const castDevice = useCastDevice();
const castState = useCastState();
const mediaStatus = useMediaStatus();
// Local state
const [state, setState] = useState<CastPlayerState>(DEFAULT_CAST_STATE);
const lastReportedProgressRef = useRef(0);
const volumeDebounceRef = useRef<NodeJS.Timeout | null>(null);
const hasReportedStartRef = useRef<string | null>(null); // Track which item we reported start for
const stateRef = useRef<CastPlayerState>(DEFAULT_CAST_STATE); // Ref for progress reporting without deps
// Helper to update both state and ref
const updateState = useCallback(
(updater: (prev: CastPlayerState) => CastPlayerState) => {
setState((prev) => {
const next = updater(prev);
stateRef.current = next;
return next;
});
},
[],
);
// Real Jellyfin PlaySessionId, embedded in customData by buildCastMediaInfo.
const playSessionId =
(
mediaStatus?.mediaInfo?.customData as
| { playSessionId?: string }
| undefined
)?.playSessionId ?? mediaStatus?.mediaInfo?.contentId;
const playMethod =
(
mediaStatus?.mediaInfo?.customData as
| { playMethod?: "Transcode" | "DirectPlay" }
| undefined
)?.playMethod ?? "Transcode";
// Detect which protocol is active - use CastState for reliable detection
const chromecastConnected = castState === CastState.CONNECTED;
// Future: Add detection for other protocols here
const activeProtocol: CastProtocol | null = chromecastConnected
? "chromecast"
: null;
const isConnected = chromecastConnected;
// Update current device
useEffect(() => {
if (chromecastConnected && castDevice) {
updateState((prev) => ({
...prev,
isConnected: true,
protocol: "chromecast",
currentDevice: {
id: castDevice.deviceId,
name: castDevice.friendlyName || castDevice.deviceId,
protocol: "chromecast",
},
}));
} else {
updateState((prev) => ({
...prev,
isConnected: false,
protocol: null,
currentDevice: null,
}));
}
// Future: Add device detection for other protocols
}, [chromecastConnected, castDevice]);
// Chromecast: Update playback state
useEffect(() => {
if (activeProtocol === "chromecast" && mediaStatus) {
updateState((prev) => ({
...prev,
isPlaying: mediaStatus.playerState === "playing",
progress: (mediaStatus.streamPosition || 0) * 1000,
duration: (mediaStatus.mediaInfo?.streamDuration || 0) * 1000,
isBuffering: mediaStatus.playerState === "buffering",
}));
}
}, [mediaStatus, activeProtocol, updateState]);
// Chromecast: Sync volume from mediaStatus
useEffect(() => {
if (activeProtocol !== "chromecast") return;
// Sync from mediaStatus when available
if (mediaStatus?.volume !== undefined) {
updateState((prev) => ({
...prev,
volume: mediaStatus.volume,
}));
}
}, [mediaStatus?.volume, activeProtocol, updateState]);
// Progress reporting to Jellyfin (matches native player behavior)
// Uses stateRef to read current progress/volume without adding them as deps
useEffect(() => {
if (!isConnected || !item?.Id || !user?.Id || !api) return;
const playStateApi = getPlaystateApi(api);
// Report playback start when media begins (only once per item)
// Don't require progress > 0 — playback can legitimately start at position 0
const currentState = stateRef.current;
const isPlaybackActive =
currentState.isPlaying ||
mediaStatus?.playerState === "playing" ||
currentState.progress > 0;
if (hasReportedStartRef.current !== item.Id && isPlaybackActive) {
// Set synchronously before async call to prevent race condition duplicates
hasReportedStartRef.current = item.Id || null;
playStateApi
.reportPlaybackStart({
playbackStartInfo: {
ItemId: item.Id,
PositionTicks: Math.floor(currentState.progress * 10000),
PlayMethod: playMethod,
VolumeLevel: Math.floor(currentState.volume * 100),
IsMuted: currentState.volume === 0,
PlaySessionId: playSessionId,
},
})
.catch((error) => {
// Revert on failure so it can be retried
hasReportedStartRef.current = null;
console.error("[useCasting] Failed to report playback start:", error);
});
}
const reportProgress = () => {
const s = stateRef.current;
// Don't report if no meaningful progress or if buffering
if (s.progress <= 0 || s.isBuffering) return;
const progressMs = Math.floor(s.progress);
const progressTicks = progressMs * 10000; // Convert ms to ticks
const progressSeconds = Math.floor(progressMs / 1000);
// When paused, always report to keep server in sync
// When playing, skip if progress hasn't changed significantly (less than 3 seconds)
if (
s.isPlaying &&
Math.abs(progressSeconds - lastReportedProgressRef.current) < 3
) {
return;
}
lastReportedProgressRef.current = progressSeconds;
playStateApi
.reportPlaybackProgress({
playbackProgressInfo: {
ItemId: item.Id,
PositionTicks: progressTicks,
IsPaused: !s.isPlaying,
PlayMethod: playMethod,
VolumeLevel: Math.floor(s.volume * 100),
IsMuted: s.volume === 0,
PlaySessionId: playSessionId,
},
})
.catch((error) => {
console.error("[useCasting] Failed to report progress:", error);
});
};
// Report progress on a fixed interval, reading latest state from ref
const interval = setInterval(reportProgress, 10000);
return () => clearInterval(interval);
}, [
api,
item?.Id,
user?.Id,
isConnected,
activeProtocol,
playSessionId,
playMethod,
]);
// Play/Pause controls
const play = useCallback(async () => {
if (activeProtocol === "chromecast") {
// Check if there's an active media session
if (!client || !mediaStatus?.mediaInfo) {
console.warn(
"[useCasting] Cannot play - no active media session. Media needs to be loaded first.",
);
return;
}
try {
await client.play();
} catch (error) {
console.error("[useCasting] Error playing:", error);
throw error;
}
}
// Future: Add play control for other protocols
}, [client, mediaStatus, activeProtocol]);
const pause = useCallback(async () => {
if (activeProtocol === "chromecast") {
try {
await client?.pause();
} catch (error) {
console.error("[useCasting] Error pausing:", error);
throw error;
}
}
// Future: Add pause control for other protocols
}, [client, activeProtocol]);
const togglePlayPause = useCallback(async () => {
if (state.isPlaying) {
await pause();
} else {
await play();
}
}, [state.isPlaying, play, pause]);
// Seek controls
const seek = useCallback(
async (positionMs: number) => {
// Validate position
if (positionMs < 0 || !Number.isFinite(positionMs)) {
console.error("[useCasting] Invalid seek position (ms):", positionMs);
return;
}
const positionSeconds = positionMs / 1000;
// Additional validation for Chromecast
if (activeProtocol === "chromecast") {
// state.duration is in ms, positionSeconds is in seconds - compare in same unit
// Only clamp when duration is known (> 0) to avoid forcing seeks to 0
const durationSeconds = state.duration / 1000;
if (durationSeconds > 0 && positionSeconds > durationSeconds) {
console.warn(
"[useCasting] Seek position exceeds duration, clamping:",
positionSeconds,
"->",
durationSeconds,
);
await client?.seek({ position: durationSeconds });
return;
}
await client?.seek({ position: positionSeconds });
}
// Future: Add seek control for other protocols
},
[client, activeProtocol, state.duration],
);
const skipForward = useCallback(
async (seconds = 10) => {
const newPosition = state.progress + seconds * 1000;
await seek(Math.min(newPosition, state.duration));
},
[state.progress, state.duration, seek],
);
const skipBackward = useCallback(
async (seconds = 10) => {
const newPosition = state.progress - seconds * 1000;
await seek(Math.max(newPosition, 0));
},
[state.progress, seek],
);
// Stop and disconnect
const stop = useCallback(
async (onStopComplete?: () => void) => {
try {
if (activeProtocol === "chromecast") {
await client?.stop();
}
// Future: Add stop control for other protocols
// Report stop to Jellyfin
if (api && item?.Id && user?.Id) {
const playStateApi = getPlaystateApi(api);
await playStateApi.reportPlaybackStopped({
playbackStopInfo: {
ItemId: item.Id,
PositionTicks: stateRef.current.progress * 10000,
},
});
}
} catch (error) {
console.error("[useCasting] Error during stop:", error);
} finally {
hasReportedStartRef.current = null;
setState(DEFAULT_CAST_STATE);
stateRef.current = DEFAULT_CAST_STATE;
// Call callback after stop completes (e.g., to navigate away)
if (onStopComplete) {
onStopComplete();
}
}
},
[client, api, item?.Id, user?.Id, activeProtocol],
);
// Volume control (debounced to reduce API calls)
const setVolume = useCallback(
(volume: number) => {
const clampedVolume = Math.max(0, Math.min(1, volume));
// Update UI immediately
updateState((prev) => ({ ...prev, volume: clampedVolume }));
// Debounce API call
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
volumeDebounceRef.current = setTimeout(async () => {
if (activeProtocol === "chromecast" && client && isConnected) {
// Use setStreamVolume for media stream volume (0.0 - 1.0)
// Physical volume buttons are handled automatically by the framework
await client.setStreamVolume(clampedVolume).catch(() => {
// Ignore errors - session might have ended
});
}
// Future: Add volume control for other protocols
}, 300);
},
[client, activeProtocol, isConnected],
);
// Cleanup
useEffect(() => {
return () => {
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
};
}, []);
return {
// State
isConnected,
protocol: activeProtocol,
isPlaying: state.isPlaying,
isBuffering: state.isBuffering,
currentItem: item,
currentDevice: state.currentDevice,
progress: state.progress,
duration: state.duration,
volume: state.volume,
// Availability - derived from actual cast state
isChromecastAvailable:
castState === CastState.CONNECTED ||
castState === CastState.CONNECTING ||
castState === CastState.NOT_CONNECTED,
// Raw clients (for advanced operations)
remoteMediaClient: client,
// Controls
play,
pause,
togglePlayPause,
seek,
skipForward,
skipBackward,
stop,
setVolume,
};
};

View File

@@ -109,35 +109,30 @@ export const usePlaybackManager = ({
staleTime: 0, staleTime: 0,
}); });
/**
* Derive prev/next from the current item's real position in the adjacent
* list rather than from the array length. `getEpisodes({ adjacentTo })` does
* not guarantee a fixed [prev, current, next] shape — at the first/last
* episode it can still return the current item as the first/last entry — so
* length-based indexing wrongly surfaces the current episode as "previous".
*/
const currentIndex = useMemo(
() => adjacentItems?.findIndex((e) => e.Id === item?.Id) ?? -1,
[adjacentItems, item],
);
/** A neighbour is only navigable if it has an actual media file (not a
* "Virtual"/missing episode placeholder, e.g. an absent Special). */
const isNavigable = (episode?: BaseItemDto | null): episode is BaseItemDto =>
!!episode && episode.Id !== item?.Id && episode.LocationType !== "Virtual";
const previousItem = useMemo(() => { const previousItem = useMemo(() => {
if (!adjacentItems || currentIndex <= 0) return null; if (!adjacentItems || adjacentItems.length <= 1) {
const candidate = adjacentItems[currentIndex - 1]; return null;
return isNavigable(candidate) ? candidate : null; }
}, [adjacentItems, currentIndex, item]);
if (adjacentItems.length === 2) {
return adjacentItems[0].Id === item?.Id ? null : adjacentItems[0];
}
return adjacentItems[0];
}, [adjacentItems, item]);
/** The next item in the series */ /** The next item in the series */
const nextItem = useMemo(() => { const nextItem = useMemo(() => {
if (!adjacentItems || currentIndex < 0) return null; if (!adjacentItems || adjacentItems.length <= 1) {
const candidate = adjacentItems[currentIndex + 1]; return null;
return isNavigable(candidate) ? candidate : null; }
}, [adjacentItems, currentIndex, item]);
if (adjacentItems.length === 2) {
return adjacentItems[1].Id === item?.Id ? null : adjacentItems[1];
}
return adjacentItems[2];
}, [adjacentItems, item]);
/** /**
* Reports playback progress. * Reports playback progress.

View File

@@ -1,64 +0,0 @@
/**
* Dispatches Jellyfin remote-control WebSocket messages to the active
* PlaybackController. DisplayMessage is shown as an in-app toast and needs no
* controller.
*/
import { useAtomValue } from "jotai";
import { useEffect, useRef } from "react";
import { toast } from "sonner-native";
import { activePlaybackControllerAtom } from "@/utils/playback/playbackController";
import {
mapRemoteCommand,
type RemoteWsMessage,
} from "@/utils/playback/remoteCommands";
/** Handle one remote-control message (call it whenever a new WS message arrives). */
export const useRemoteControl = (lastMessage: RemoteWsMessage | null): void => {
const controller = useAtomValue(activePlaybackControllerAtom);
const handledRef = useRef<RemoteWsMessage | null>(null);
useEffect(() => {
if (!lastMessage || lastMessage === handledRef.current) return;
handledRef.current = lastMessage;
const action = mapRemoteCommand(lastMessage);
if (!action) return;
if (action.kind === "displayMessage") {
toast(action.text);
return;
}
if (!controller) return;
switch (action.kind) {
case "playPause":
controller.playPause();
break;
case "pause":
controller.pause();
break;
case "unpause":
controller.unpause();
break;
case "stop":
controller.stop();
break;
case "seek":
controller.seek(action.positionMs);
break;
case "next":
controller.next();
break;
case "previous":
controller.previous();
break;
case "setVolume":
controller.setVolume(action.level);
break;
case "toggleMute":
controller.toggleMute();
break;
}
}, [lastMessage, controller]);
};

View File

@@ -1,113 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
import { MediaTimeSegment } from "@/providers/Downloads/types";
import { useSettings } from "@/utils/atoms/settings";
import { useHaptic } from "./useHaptic";
type SegmentType = "Intro" | "Outro" | "Recap" | "Commercial" | "Preview";
interface UseSegmentSkipperProps {
segments: MediaTimeSegment[];
segmentType: SegmentType;
currentTime: number;
totalDuration?: number;
seek: (time: number) => void;
isPaused: boolean;
}
interface UseSegmentSkipperReturn {
currentSegment: MediaTimeSegment | null;
skipSegment: (notifyOrUseHaptics?: boolean) => void;
}
/**
* Generic hook to handle all media segment types (intro, outro, recap, commercial, preview)
* Supports three modes: 'none' (disabled), 'ask' (show button), 'auto' (auto-skip)
*/
export const useSegmentSkipper = ({
segments,
segmentType,
currentTime,
totalDuration,
seek,
isPaused,
}: UseSegmentSkipperProps): UseSegmentSkipperReturn => {
const { settings } = useSettings();
const haptic = useHaptic();
const autoSkipTriggeredRef = useRef<string | null>(null);
// Get skip mode based on segment type
const skipMode = (() => {
switch (segmentType) {
case "Intro":
return settings.skipIntro;
case "Outro":
return settings.skipOutro;
case "Recap":
return settings.skipRecap;
case "Commercial":
return settings.skipCommercial;
case "Preview":
return settings.skipPreview;
default:
return "none";
}
})();
// Find current segment
const currentSegment =
segments.find(
(segment) =>
currentTime >= segment.startTime && currentTime < segment.endTime,
) || null;
// Skip function with optional haptic feedback
const skipSegment = useCallback(
(notifyOrUseHaptics = true) => {
if (!currentSegment || skipMode === "none") return;
// For Outro segments, prevent seeking past the end
if (
segmentType === "Outro" &&
totalDuration != null &&
Number.isFinite(totalDuration)
) {
const seekTime = Math.min(currentSegment.endTime, totalDuration);
seek(seekTime);
} else {
seek(currentSegment.endTime);
}
// Only trigger haptic feedback if explicitly requested (manual skip)
if (notifyOrUseHaptics) {
haptic();
}
},
[currentSegment, segmentType, totalDuration, seek, haptic, skipMode],
);
// Auto-skip logic when mode is 'auto'
useEffect(() => {
if (skipMode !== "auto" || isPaused) {
return;
}
// Track segment identity to avoid re-triggering on pause/unpause
const segmentId = currentSegment
? `${currentSegment.startTime}-${currentSegment.endTime}`
: null;
if (currentSegment && autoSkipTriggeredRef.current !== segmentId) {
autoSkipTriggeredRef.current = segmentId;
skipSegment(false); // Don't trigger haptics for auto-skip
}
if (!currentSegment) {
autoSkipTriggeredRef.current = null;
}
}, [currentSegment, skipMode, isPaused, skipSegment]);
// Return null segment if skip mode is 'none'
return {
currentSegment: skipMode === "none" ? null : currentSegment,
skipSegment,
};
};

View File

@@ -17,24 +17,20 @@ interface TrickplayUrl {
} }
/** Hook to handle trickplay logic for a given item. */ /** Hook to handle trickplay logic for a given item. */
export const useTrickplay = (item: BaseItemDto | null) => { export const useTrickplay = (item: BaseItemDto) => {
const { getDownloadedItemById } = useDownload(); const { getDownloadedItemById } = useDownload();
const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null); const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null);
const lastCalculationTime = useRef(0); const lastCalculationTime = useRef(0);
const throttleDelay = 200; const throttleDelay = 200;
const isOffline = useGlobalSearchParams().offline === "true"; const isOffline = useGlobalSearchParams().offline === "true";
const trickplayInfo = useMemo( const trickplayInfo = useMemo(() => getTrickplayInfo(item), [item]);
() => (item ? getTrickplayInfo(item) : null),
[item],
);
/** Generates the trickplay URL for the given item and sheet index. /** Generates the trickplay URL for the given item and sheet index.
* We change between offline and online trickplay URLs depending on the state of the app. */ * We change between offline and online trickplay URLs depending on the state of the app. */
const getTrickplayUrl = useCallback( const getTrickplayUrl = useCallback(
(item: BaseItemDto, sheetIndex: number) => { (item: BaseItemDto, sheetIndex: number) => {
if (!item.Id) return null;
// If we are offline, we can use the downloaded item's trickplay data path // If we are offline, we can use the downloaded item's trickplay data path
const downloadedItem = getDownloadedItemById(item.Id); const downloadedItem = getDownloadedItemById(item.Id!);
if (isOffline && downloadedItem?.trickPlayData?.path) { if (isOffline && downloadedItem?.trickPlayData?.path) {
return `${downloadedItem.trickPlayData.path}${sheetIndex}.jpg`; return `${downloadedItem.trickPlayData.path}${sheetIndex}.jpg`;
} }
@@ -49,7 +45,7 @@ export const useTrickplay = (item: BaseItemDto | null) => {
const now = Date.now(); const now = Date.now();
if ( if (
!trickplayInfo || !trickplayInfo ||
!item?.Id || !item.Id ||
now - lastCalculationTime.current < throttleDelay now - lastCalculationTime.current < throttleDelay
) )
return; return;
@@ -66,7 +62,7 @@ export const useTrickplay = (item: BaseItemDto | null) => {
/** Prefetches all the trickplay images for the item, limiting concurrency to avoid I/O spikes. */ /** Prefetches all the trickplay images for the item, limiting concurrency to avoid I/O spikes. */
const prefetchAllTrickplayImages = useCallback(async () => { const prefetchAllTrickplayImages = useCallback(async () => {
if (!trickplayInfo || !item?.Id) return; if (!trickplayInfo || !item.Id) return;
const maxConcurrent = 4; const maxConcurrent = 4;
const total = trickplayInfo.totalImageSheets; const total = trickplayInfo.totalImageSheets;
const urls: string[] = []; const urls: string[] = [];

View File

@@ -22,7 +22,9 @@
"lint": "biome check --write --unsafe --max-diagnostics 1000", "lint": "biome check --write --unsafe --max-diagnostics 1000",
"format": "biome format --write .", "format": "biome format --write .",
"doctor": "expo-doctor", "doctor": "expo-doctor",
"test": "bun run typecheck && bun run lint && bun run format && bun run doctor", "i18n:check": "bun scripts/check-i18n-keys.mjs",
"i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused",
"test": "bun run typecheck && bun run lint && bun run format && bun run i18n:check && bun run doctor",
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {

View File

@@ -32,6 +32,12 @@ export interface MediaTimeSegment {
text: string; text: string;
} }
export interface Segment {
startTime: number;
endTime: number;
text: string;
}
/** Represents a single downloaded media item with all necessary metadata for offline playback. */ /** Represents a single downloaded media item with all necessary metadata for offline playback. */
export interface DownloadedItem { export interface DownloadedItem {
/** The Jellyfin item DTO. */ /** The Jellyfin item DTO. */
@@ -50,12 +56,6 @@ export interface DownloadedItem {
introSegments?: MediaTimeSegment[]; introSegments?: MediaTimeSegment[];
/** The credit segments for the item. */ /** The credit segments for the item. */
creditSegments?: MediaTimeSegment[]; creditSegments?: MediaTimeSegment[];
/** The recap segments for the item. */
recapSegments?: MediaTimeSegment[];
/** The commercial segments for the item. */
commercialSegments?: MediaTimeSegment[];
/** The preview segments for the item. */
previewSegments?: MediaTimeSegment[];
/** The user data for the item. */ /** The user data for the item. */
userData: UserData; userData: UserData;
} }
@@ -144,12 +144,6 @@ export type JobStatus = {
introSegments?: MediaTimeSegment[]; introSegments?: MediaTimeSegment[];
/** Pre-downloaded credit segments (optional) - downloaded before video starts */ /** Pre-downloaded credit segments (optional) - downloaded before video starts */
creditSegments?: MediaTimeSegment[]; creditSegments?: MediaTimeSegment[];
/** Pre-downloaded recap segments (optional) - downloaded before video starts */
recapSegments?: MediaTimeSegment[];
/** Pre-downloaded commercial segments (optional) - downloaded before video starts */
commercialSegments?: MediaTimeSegment[];
/** Pre-downloaded preview segments (optional) - downloaded before video starts */
previewSegments?: MediaTimeSegment[];
/** The audio stream index selected for this download */ /** The audio stream index selected for this download */
audioStreamIndex?: number; audioStreamIndex?: number;
/** The subtitle stream index selected for this download */ /** The subtitle stream index selected for this download */

View File

@@ -28,10 +28,6 @@ import { useNetworkStatus } from "@/providers/NetworkStatusProvider";
import { settingsAtom } from "@/utils/atoms/settings"; import { settingsAtom } from "@/utils/atoms/settings";
import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import {
type PlaybackController,
useRegisterPlaybackController,
} from "@/utils/playback/playbackController";
// Conditionally import TrackPlayer only on non-TV platforms // Conditionally import TrackPlayer only on non-TV platforms
// This prevents the native module from being loaded on TV where it doesn't exist // This prevents the native module from being loaded on TV where it doesn't exist
@@ -1625,43 +1621,6 @@ const MobileMusicPlayerProvider: React.FC<MusicPlayerProviderProps> = ({
settings?.audioLookaheadCount, settings?.audioLookaheadCount,
]); ]);
// App-wide remote-control surface: wraps the existing music controls so
// remote commands can target whatever player is currently active.
const isMusicActive = state.currentTrack !== null;
const playbackController = useMemo<PlaybackController>(
() => ({
playPause: () => {
togglePlayPause();
},
pause: () => {
pause();
},
unpause: () => {
resume();
},
stop: () => {
stop();
},
// TrackPlayer works in seconds; the controller contract is milliseconds.
seek: (positionMs: number) => {
seek(positionMs / 1000);
},
next: () => {
next();
},
previous: () => {
previous();
},
// The music player exposes no volume API — keep these as no-ops.
setVolume: () => {},
toggleMute: () => {},
}),
[togglePlayPause, pause, resume, stop, seek, next, previous],
);
useRegisterPlaybackController(playbackController, isMusicActive);
const value = useMemo( const value = useMemo(
() => ({ () => ({
...state, ...state,

View File

@@ -13,7 +13,6 @@ import {
import { AppState, type AppStateStatus } from "react-native"; import { AppState, type AppStateStatus } from "react-native";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient"; import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
import { useRemoteControl } from "@/hooks/useRemoteControl";
import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider"; import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider";
import { useNetworkStatus } from "@/providers/NetworkStatusProvider"; import { useNetworkStatus } from "@/providers/NetworkStatusProvider";
@@ -55,8 +54,6 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
const [ws, setWs] = useState<WebSocket | null>(null); const [ws, setWs] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null); const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null);
// Route Jellyfin remote-control messages to the active player.
useRemoteControl(lastMessage);
const router = useRouter(); const router = useRouter();
const queryClient = useNetworkAwareQueryClient(); const queryClient = useNetworkAwareQueryClient();
const deviceId = useMemo(() => { const deviceId = useMemo(() => {
@@ -222,14 +219,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
IconUrl: IconUrl:
"https://raw.githubusercontent.com/retardgerman/streamyfinweb/refs/heads/main/public/assets/images/icon_new_withoutBackground.png", "https://raw.githubusercontent.com/retardgerman/streamyfinweb/refs/heads/main/public/assets/images/icon_new_withoutBackground.png",
PlayableMediaTypes: ["Audio", "Video"], PlayableMediaTypes: ["Audio", "Video"],
SupportedCommands: [ SupportedCommands: ["Play"],
"Play",
"DisplayMessage",
"SetVolume",
"ToggleMute",
"Mute",
"Unmute",
],
SupportsMediaControl: true, SupportsMediaControl: true,
SupportsPersistentIdentifier: true, SupportsPersistentIdentifier: true,
}, },

268
scripts/check-i18n-keys.mjs Normal file
View File

@@ -0,0 +1,268 @@
#!/usr/bin/env bun
/**
* i18n key checker for Streamyfin.
*
* Detects:
* - MISSING keys: a static `t("a.b.c")` / `i18nKey="a.b.c"` referenced in the code
* that does not exist in the source locale (translations/en.json). These are bugs —
* the app renders the raw key. Always fails CI.
* - UNUSED (dead) keys: a key in the source locale that is referenced nowhere in the
* code, neither statically nor via a detected dynamic prefix (`t(`a.b.${x}`)`).
* These are dead weight that also clutter every locale on Crowdin.
*
* Dynamic usage is handled conservatively:
* - `t(`prefix.${x}`)` -> every key starting with `prefix.` is considered used.
* - `t(`${x}`)` -> fully dynamic, reported for manual review, never used to
* whitelist keys (in Streamyfin these are user-defined section
* titles, not translation keys).
* - Edge cases the static scan cannot see can be allow-listed in the config file.
*
* Usage:
* bun scripts/check-i18n-keys.mjs # report + exit 1 on missing OR unused
* bun scripts/check-i18n-keys.mjs --unused=warn # exit 1 only on missing; unused = warning
* bun scripts/check-i18n-keys.mjs --unused=off # ignore unused entirely
* bun scripts/check-i18n-keys.mjs --json # machine-readable output
* bun scripts/check-i18n-keys.mjs --fix-unused # remove dead keys from en.json (Crowdin syncs the rest)
*/
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { extname, join, relative } from "node:path";
const ROOT = process.cwd();
const args = process.argv.slice(2);
const flag = (name, def) => {
const a = args.find((x) => x === `--${name}` || x.startsWith(`--${name}=`));
if (!a) return def;
const [, v] = a.split("=");
return v === undefined ? true : v;
};
const UNUSED_MODE = String(flag("unused", "error")); // error | warn | off
const JSON_OUT = !!flag("json", false);
const FIX_UNUSED = !!flag("fix-unused", false);
// ---- config ----
const CONFIG_PATH = join(ROOT, "scripts", "i18n-keys.config.json");
const DEFAULT_CONFIG = {
localesDir: "translations",
sourceLocale: "en",
// Scan the whole repo by default so keys referenced outside the obvious dirs
// (e.g. packages/, key constants in utils/atoms) are not wrongly flagged as dead.
srcDirs: ["."],
srcExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
excludeDirs: [
"node_modules",
"ios",
"android",
".expo",
".git",
"dist",
"build",
"translations",
"scripts",
],
// Keys (or glob-ish prefixes ending with .* or *) known to be used dynamically / externally.
ignoreUnused: [],
};
const config = existsSync(CONFIG_PATH)
? { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) }
: DEFAULT_CONFIG;
// ---- helpers ----
const flatten = (obj, prefix = "", out = {}) => {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
else out[key] = v;
}
return out;
};
const globMatch = (key, pattern) => {
if (pattern.endsWith(".*"))
return key === pattern.slice(0, -2) || key.startsWith(pattern.slice(0, -1));
if (pattern.endsWith("*")) return key.startsWith(pattern.slice(0, -1));
return key === pattern;
};
const walk = (dir, files = []) => {
let entries;
try {
entries = readdirSync(dir);
} catch {
return files;
}
for (const name of entries) {
const full = join(dir, name);
let st;
try {
st = statSync(full);
} catch {
continue;
}
if (st.isDirectory()) {
if (config.excludeDirs.includes(name)) continue;
walk(full, files);
} else if (config.srcExtensions.includes(extname(name))) {
files.push(full);
}
}
return files;
};
// ---- load source keys ----
const sourcePath = join(ROOT, config.localesDir, `${config.sourceLocale}.json`);
const sourceKeys = Object.keys(
flatten(JSON.parse(readFileSync(sourcePath, "utf8"))),
);
const sourceKeySet = new Set(sourceKeys);
// ---- scan code ----
const STATIC_RE = /\bt\(\s*(['"])((?:\\.|(?!\1).)+?)\1/g; // t("a.b") / t('a.b')
const TPL_STATIC_RE = /\bt\(\s*`([^`$]+)`/g; // t(`a.b`) no interpolation
const TPL_DYN_RE = /\bt\(\s*`([^`$]*)\$\{/g; // t(`a.b.${x}`) -> prefix "a.b."
const I18NKEY_RE = /\bi18nKey\s*=\s*(?:\{\s*)?(['"])((?:\\.|(?!\1).)+?)\1/g; // <Trans i18nKey="a.b">
const KEY_SHAPE = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)+$/; // dotted key, e.g. home.x.y
const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection
const dynamicPrefixes = new Set();
const fullyDynamic = []; // { file, line }
let codeBlob = ""; // all (comment-stripped) source text — searched for delimited key literals
// Strip comments so keys mentioned in comments (e.g. `// t("old.key")`) are not counted as
// usage. Block comments and JSX {/* */} are blanked (preserving newlines for line numbers);
// line comments are only stripped when `//` follows start/whitespace/punctuation, which keeps
// `://` inside string URLs intact.
const stripComments = (src) =>
src
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
.replace(/(^|[\s;{}()[\],=>])\/\/[^\n]*/g, (_m, p) => p);
const files = config.srcDirs.flatMap((d) =>
walk(join(ROOT, d === "." ? "" : d) || ROOT),
);
for (const file of files) {
const text = readFileSync(file, "utf8");
const clean = stripComments(text);
codeBlob += `\n${clean}`;
for (const m of clean.matchAll(STATIC_RE)) usedStatic.add(m[2]);
for (const m of clean.matchAll(TPL_STATIC_RE)) usedStatic.add(m[1]);
for (const m of clean.matchAll(I18NKEY_RE)) usedStatic.add(m[2]);
for (const m of clean.matchAll(TPL_DYN_RE)) {
const prefix = m[1];
if (prefix?.includes(".")) dynamicPrefixes.add(prefix);
else {
const idx = clean.slice(0, m.index).split("\n").length;
fullyDynamic.push({ file: relative(ROOT, file), line: idx });
}
}
}
const prefixList = [...dynamicPrefixes];
// A key counts as used if its EXACT delimited literal ("k" / 'k' / `k`) appears anywhere in
// the code (covers t("k"), <Trans i18nKey>, and keys stored as bare string constants in
// arrays/config then resolved via t(variable)), or it is reached via a dynamic prefix, or
// explicitly allow-listed. Delimited search avoids substring false-matches (e.g. a.b vs a.b_c).
const literalUsed = (key) =>
codeBlob.includes(`"${key}"`) ||
codeBlob.includes(`'${key}'`) ||
codeBlob.includes(`\`${key}\``);
const isUsed = (key) =>
literalUsed(key) ||
prefixList.some((p) => key.startsWith(p)) ||
config.ignoreUnused.some((g) => globMatch(key, g));
// ---- compute ----
const unused = sourceKeys.filter((k) => !isUsed(k)).sort();
// Static references are always validated, even under a dynamic prefix: a dynamic prefix only
// affects the UNUSED calculation, never MISSING.
const missing = [...usedStatic]
.filter((k) => KEY_SHAPE.test(k) && !sourceKeySet.has(k))
.sort();
// ---- optional fix: strip dead keys from the source locale (en.json) ----
const removeKey = (obj, parts) => {
const [head, ...rest] = parts;
if (!(head in obj)) return;
if (rest.length === 0) {
delete obj[head];
return;
}
removeKey(obj[head], rest);
if (
obj[head] &&
typeof obj[head] === "object" &&
Object.keys(obj[head]).length === 0
)
delete obj[head];
};
if (FIX_UNUSED && unused.length) {
// Only edit the SOURCE locale (en.json). Crowdin owns the target locales and removes
// the keys from them automatically on the next sync once they disappear from the source.
const data = JSON.parse(readFileSync(sourcePath, "utf8"));
for (const key of unused) removeKey(data, key.split("."));
writeFileSync(sourcePath, `${JSON.stringify(data, null, 2)}\n`);
console.log(
`🧹 Removed ${unused.length} dead key(s) from ${config.sourceLocale}.json (Crowdin will sync the other locales).`,
);
}
// ---- report ----
if (JSON_OUT) {
console.log(
JSON.stringify(
{
sourceKeys: sourceKeys.length,
missing,
unused,
dynamicPrefixes: prefixList,
fullyDynamic,
},
null,
2,
),
);
} else {
console.log(
`🔑 i18n key check — source: ${relative(ROOT, sourcePath)} (${sourceKeys.length} keys), scanned ${files.length} files`,
);
if (prefixList.length)
console.log(
` dynamic prefixes treated as used: ${prefixList.map((p) => `${p}*`).join(", ")}`,
);
if (fullyDynamic.length)
console.log(
` ⚠️ ${fullyDynamic.length} fully-dynamic t(\`\${…}\`) call(s) (not key-based, manual review): ${fullyDynamic.map((d) => `${d.file}:${d.line}`).join(", ")}`,
);
if (missing.length) {
console.log(
`\n❌ MISSING keys (used in code, absent from ${config.sourceLocale}.json) — ${missing.length}:`,
);
for (const k of missing) console.log(` - ${k}`);
} else console.log("\n✅ No missing keys.");
if (UNUSED_MODE !== "off") {
if (unused.length) {
console.log(
`\n${UNUSED_MODE === "error" ? "❌" : "⚠️ "} UNUSED keys (in ${config.sourceLocale}.json, referenced nowhere) — ${unused.length}:`,
);
for (const k of unused) console.log(` - ${k}`);
console.log(
`\n → remove with: bun scripts/check-i18n-keys.mjs --fix-unused`,
);
console.log(
` → or allow-list a dynamic key in scripts/i18n-keys.config.json ("ignoreUnused").`,
);
} else console.log("\n✅ No unused keys.");
}
}
const fail =
missing.length > 0 || (UNUSED_MODE === "error" && unused.length > 0);
process.exit(fail ? 1 : 0);

View File

@@ -0,0 +1,43 @@
{
"localesDir": "translations",
"sourceLocale": "en",
"srcDirs": [
"app",
"components",
"hooks",
"providers",
"utils",
"modules",
"packages",
"constants"
],
"srcExtensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
"excludeDirs": [
"node_modules",
"ios",
"android",
".expo",
".git",
"dist",
"build",
"translations"
],
"_ignoreUnusedNote": "Keys for planned features that are intentionally kept in en.json but not yet wired in code. They are exempt from the unused-key check until implemented. Remove an entry once its feature ships and uses the key.",
"ignoreUnused": [
"watchlists.add_to_watchlist",
"watchlists.remove_from_watchlist",
"watchlists.create_one_first",
"watchlists.no_compatible_watchlists",
"pin.confirm_pin",
"pin.pins_dont_match",
"player.search_subtitles",
"player.subtitle_search",
"player.subtitle_download_hint",
"player.subtitle_tracks",
"player.using_jellyfin_server",
"player.swipe_down_settings",
"player.stopPlayback",
"player.stopPlayingTitle",
"player.stopPlayingConfirm"
]
}

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "تابع و التالي", "continue_and_next_up": "تابع و التالي",
"recently_added_in": "أضيف مؤخراً في {{libraryName}}", "recently_added_in": "أضيف مؤخراً في {{libraryName}}",
"suggested_movies": "أفلام مقترحة", "suggested_movies": "أفلام مقترحة",
"suggested_episodes": "حلقات مقترحة",
"intro": { "intro": {
"welcome_to_streamyfin": "مرحبًا بك في Streamyfin", "welcome_to_streamyfin": "مرحبًا بك في Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "عميل مجاني ومفتوح المصدر لـJellyfin.", "a_free_and_open_source_client_for_jellyfin": "عميل مجاني ومفتوح المصدر لـJellyfin.",
@@ -261,43 +260,6 @@
"None": "لا شيء", "None": "لا شيء",
"OnlyForced": "فقط الإجبارية" "OnlyForced": "فقط الإجبارية"
}, },
"text_color": "لون النص",
"background_color": "لون الخلفية",
"outline_color": "لون إطار الخط",
"outline_thickness": "سمك إطار الخط",
"background_opacity": "شفافية الخلفية",
"outline_opacity": "شفافية إطار الخط",
"bold_text": "خط عريض",
"colors": {
"Black": "أسود",
"Gray": "رمادي",
"Silver": "فضي",
"White": "أبيض",
"Maroon": "أحمر داكن",
"Red": "أحمر",
"Fuchsia": "وردي",
"Yellow": "أصفر",
"Olive": "‫أخضر زيتوني‬‎",
"Green": "أخضر",
"Teal": "أزرق مخضر",
"Lime": "ليموني",
"Purple": "بنفسجي",
"Navy": "كحلي",
"Blue": "أزرق",
"Aqua": "أزرق بحري"
},
"thickness": {
"None": "لا شيء",
"Thin": "نحيف",
"Normal": "عادي",
"Thick": "سميك"
},
"subtitle_color": "لون الترجمة",
"subtitle_background_color": "لون الخلفية",
"subtitle_font": "خط الترجمة",
"ksplayer_title": "إعدادات KSPlayer",
"hardware_decode": "فك الترميز بواسطة الجهاز",
"hardware_decode_description": "استخدم تسريع العتاد لفك ترميز الفيديو. قم بتعطيله إذا واجهت مشكلات في التشغيل.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "إعدادات ترجمة VLC",
"hint": "تخصيص مظهر الترجمة لمشغل VLC. تصبح التغييرات سارية المفعول عند التشغيل التالي.",
"text_color": "لون النص",
"background_color": "لون الخلفية",
"background_opacity": "شفافية الخلفية",
"outline_color": "لون إطار الخط",
"outline_opacity": "شفافية إطار الخط",
"outline_thickness": "سمك إطار الخط",
"bold": "خط عريض",
"margin": "الهامش السفلي"
},
"video_player": {
"title": "مشغل الفيديو",
"video_player": "مشغل الفيديو",
"video_player_description": "اختر مشغل الفيديو الذي سيتم استخدامه على نظام iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "أخرى", "other_title": "أخرى",
"video_orientation": "اتجاه الفيديو", "video_orientation": "اتجاه الفيديو",
@@ -351,13 +294,7 @@
"UNKNOWN": "غير معروف" "UNKNOWN": "غير معروف"
}, },
"safe_area_in_controls": "المنطقة الآمنة لعناصر التحكم", "safe_area_in_controls": "المنطقة الآمنة لعناصر التحكم",
"video_player": "مشغل الفيديو",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (تجريبي + صورة داخل صورة)"
},
"show_custom_menu_links": "إظهار روابط القائمة المخصصة", "show_custom_menu_links": "إظهار روابط القائمة المخصصة",
"show_large_home_carousel": "إظهار شريط العرض الكبير (تجريبي)",
"hide_libraries": "إخفاء المكتبات", "hide_libraries": "إخفاء المكتبات",
"select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.", "select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.",
"disable_haptic_feedback": "تعطيل ردود الفعل اللمسية", "disable_haptic_feedback": "تعطيل ردود الفعل اللمسية",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا", "max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا",
"disabled": "معطل" "disabled": "معطل"
}, },
"downloads": {
"downloads_title": "التنزيلات"
},
"music": { "music": {
"title": "الموسيقى", "title": "الموسيقى",
"playback_title": "التشغيل", "playback_title": "التشغيل",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "اقرأ المزيد عن مارلن.", "read_more_about_marlin": "اقرأ المزيد عن مارلن.",
"save_button": "حفظ", "save_button": "حفظ",
"toasts": { "toasts": {
"saved": "تم الحفظ", "saved": "تم الحفظ"
"refreshed": "تم تحديث الإعدادات من الخادم" }
},
"refresh_from_server": "تحديث الإعدادات من الخادم"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "تفعيل Streamystats",
"disable_streamystats": "تعطيل Streamystats", "disable_streamystats": "تعطيل Streamystats",
"enable_search": "استخدم للبحث", "enable_search": "استخدم للبحث",
"url": "الرابط", "url": "الرابط",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "أدخل رابط خادم Streamystats الخاص بك. يجب أن يتضمن الرابط البروتوكول http أو https مع رقم المنفذ اختيارياً.", "streamystats_search_hint": "أدخل رابط خادم Streamystats الخاص بك. يجب أن يتضمن الرابط البروتوكول http أو https مع رقم المنفذ اختيارياً.",
"read_more_about_streamystats": "اقرأ المزيد عن Streamystats.", "read_more_about_streamystats": "اقرأ المزيد عن Streamystats.",
"save_button": "حفظ",
"save": "حفظ", "save": "حفظ",
"features_title": "المميزات", "features_title": "المميزات",
"home_sections_title": "أقسام الرئيسية",
"enable_movie_recommendations": "توصيات الأفلام", "enable_movie_recommendations": "توصيات الأفلام",
"enable_series_recommendations": "توصيات المسلسلات", "enable_series_recommendations": "توصيات المسلسلات",
"enable_promoted_watchlists": "قوائم مشاهدة مختارة", "enable_promoted_watchlists": "قوائم مشاهدة مختارة",
@@ -445,8 +374,7 @@
"refresh_from_server": "تحديث الإعدادات من الخادم" "refresh_from_server": "تحديث الإعدادات من الخادم"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "تفعيل الربط مع قائمة المشاهدة الخاصة بنا", "watchlist_enabler": "تفعيل الربط مع قائمة المشاهدة الخاصة بنا"
"watchlist_button": "تبديل حالة ربط قائمة المشاهدة"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها", "delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
"music_cache_title": "التخزين المؤقت للموسيقى", "music_cache_title": "التخزين المؤقت للموسيقى",
"music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال", "music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال",
"enable_music_cache": "تمكين التخزين المؤقت للموسيقى",
"clear_music_cache": "مسح التخزين المؤقت للموسيقى", "clear_music_cache": "مسح التخزين المؤقت للموسيقى",
"music_cache_size": "تم تخزين {{size}} مؤقتاً", "music_cache_size": "تم تخزين {{size}} مؤقتاً",
"music_cache_cleared": "تم مسح التخزين المؤقت للموسيقى", "music_cache_cleared": "تم مسح التخزين المؤقت للموسيقى",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "تصدير السجلات", "export_logs": "تصدير السجلات",
"click_for_more_info": "اضغط للمزيد من المعلومات", "click_for_more_info": "اضغط للمزيد من المعلومات",
"level": "المستوى", "level": "المستوى",
"no_logs_available": "لا توجد سجلات متاحة", "no_logs_available": "لا توجد سجلات متاحة"
"delete_all_logs": "حذف جميع السجلات"
}, },
"languages": { "languages": {
"title": "اللغات", "title": "اللغات",
@@ -490,15 +414,12 @@
"system": "النظام" "system": "النظام"
}, },
"toasts": { "toasts": {
"error_deleting_files": "خطأ في حذف الملفات", "error_deleting_files": "خطأ في حذف الملفات"
"background_downloads_enabled": "تم تفعيل التنزيلات في الخلفية",
"background_downloads_disabled": "تم تعطيل التنزيلات في الخلفية"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "التنزيلات", "downloads_title": "التنزيلات",
"tvseries": "مسلسلات", "tvseries": "مسلسلات",
"movies": "أفلام", "movies": "أفلام",
"queue": "قائمة الانتظار",
"other_media": "وسائط أخرى", "other_media": "وسائط أخرى",
"queue_hint": "ستفقد قائمة الانتظار والتنزيلات عند إعادة تشغيل التطبيق",
"no_items_in_queue": "لا توجد عناصر في قائمة الانتظار",
"no_downloaded_items": "لا توجد عناصر تم تنزيلها", "no_downloaded_items": "لا توجد عناصر تم تنزيلها",
"delete_all_movies_button": "حذف جميع الأفلام", "delete_all_movies_button": "حذف جميع الأفلام",
"delete_all_tvseries_button": "حذف جميع المسلسلات", "delete_all_tvseries_button": "حذف جميع المسلسلات",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "فشل حذف جميع المسلسلات", "failed_to_delete_all_tvseries": "فشل حذف جميع المسلسلات",
"deleted_media_successfully": "تم حذف الوسائط الأخرى بنجاح!", "deleted_media_successfully": "تم حذف الوسائط الأخرى بنجاح!",
"failed_to_delete_media": "فشل حذف الوسائط الأخرى", "failed_to_delete_media": "فشل حذف الوسائط الأخرى",
"download_deleted": "تم حذف التنزيل",
"download_cancelled": "تم إلغاء التنزيل", "download_cancelled": "تم إلغاء التنزيل",
"could_not_delete_download": "تعذر حذف التنزيل", "could_not_delete_download": "تعذر حذف التنزيل",
"download_paused": "تم إيقاف التنزيل مؤقتًا",
"could_not_pause_download": "تعذر إيقاف التنزيل مؤقتًا",
"download_resumed": "تم استئناف التنزيل",
"could_not_resume_download": "تعذر استئناف التنزيل",
"download_completed": "اكتمل التنزيل", "download_completed": "اكتمل التنزيل",
"download_failed": "فشل التنزيل", "download_failed": "فشل التنزيل",
"download_failed_for_item": "فشل تنزيل {{item}} - {{error}}", "download_failed_for_item": "فشل تنزيل {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} قيد التنزيل بالفعل", "item_already_downloading": "{{item}} قيد التنزيل بالفعل",
"all_files_deleted": "تم حذف جميع التنزيلات بنجاح", "all_files_deleted": "تم حذف جميع التنزيلات بنجاح",
"files_deleted_by_type": "تم حذف {{count}} {{type}}", "files_deleted_by_type": "تم حذف {{count}} {{type}}",
"all_files_folders_and_jobs_deleted_successfully": "تم حذف جميع الملفات والمجلدات والمهام بنجاح",
"failed_to_clean_cache_directory": "فشل تنظيف مجلد ذاكرة التخزين المؤقت",
"could_not_get_download_url_for_item": "تعذر الحصول على عنوان URL للتنزيل لـ{{itemName}}", "could_not_get_download_url_for_item": "تعذر الحصول على عنوان URL للتنزيل لـ{{itemName}}",
"go_to_downloads": "الذهاب إلى التنزيلات",
"file_deleted": "تم حذف {{item}}" "file_deleted": "تم حذف {{item}}"
} }
} }
@@ -583,16 +493,13 @@
"none": "لا شيء", "none": "لا شيء",
"track": "أغنية", "track": "أغنية",
"cancel": "إلغاء", "cancel": "إلغاء",
"stop": "Stop",
"delete": "حذف", "delete": "حذف",
"ok": "حسناً", "ok": "حسناً",
"remove": "إزالة", "remove": "إزالة",
"next": "التالي",
"back": "رجوع", "back": "رجوع",
"continue": "متابعة", "continue": "متابعة",
"verifying": "جارٍ التحقق...", "verifying": "جارٍ التحقق...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "بحث...", "search": "بحث...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "تعذر إنشاء بث لـChromecast", "could_not_create_stream_for_chromecast": "تعذر إنشاء بث لـChromecast",
"message_from_server": "رسالة من الخادم: {{message}}", "message_from_server": "رسالة من الخادم: {{message}}",
"next_episode": "الحلقة التالية", "next_episode": "الحلقة التالية",
"refresh_tracks": "تحديث المسارات",
"audio_tracks": "مسارات الصوت:",
"playback_state": "حالة التشغيل:",
"index": "الفِهْرِس:",
"continue_watching": "متابعة المشاهدة", "continue_watching": "متابعة المشاهدة",
"go_back": "رجوع", "go_back": "رجوع",
"downloaded_file_title": "تم تنزيل هذا الملف", "downloaded_file_title": "تم تنزيل هذا الملف",
@@ -761,7 +664,6 @@
"show_more": "عرض المزيد", "show_more": "عرض المزيد",
"show_less": "عرض أقل", "show_less": "عرض أقل",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "قوائم التشغيل", "playlists": "قوائم التشغيل",
"tracks": "الأغاني" "tracks": "الأغاني"
}, },
"filters": {
"all": "الكل"
},
"recently_added": "أضيف مؤخرًا", "recently_added": "أضيف مؤخرًا",
"recently_played": "تم تشغيله مؤخرًا", "recently_played": "تم تشغيله مؤخرًا",
"frequently_played": "الأكثر تشغيلاً", "frequently_played": "الأكثر تشغيلاً",
"explore": "اكتشف",
"top_tracks": "أفضل الأغاني", "top_tracks": "أفضل الأغاني",
"play": "تشغيل", "play": "تشغيل",
"shuffle": "ترتيب عشوائي", "shuffle": "ترتيب عشوائي",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Afegit recentment a {{libraryName}}", "recently_added_in": "Afegit recentment a {{libraryName}}",
"suggested_movies": "Pel·lícules suggerides", "suggested_movies": "Pel·lícules suggerides",
"suggested_episodes": "Episodis suggerits",
"intro": { "intro": {
"welcome_to_streamyfin": "Benvingut a Streamyfin", "welcome_to_streamyfin": "Benvingut a Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Un client gratuït i de codi obert per a Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Un client gratuït i de codi obert per a Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Cap", "None": "Cap",
"OnlyForced": "Només els forçats" "OnlyForced": "Només els forçats"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "Cap",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Altres", "other_title": "Altres",
"video_orientation": "Orientació del vídeo", "video_orientation": "Orientació del vídeo",
@@ -351,13 +294,7 @@
"UNKNOWN": "Desconeguda" "UNKNOWN": "Desconeguda"
}, },
"safe_area_in_controls": "Àrea segura als controls", "safe_area_in_controls": "Àrea segura als controls",
"video_player": "Reproductor de vídeo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Mostrar enllaços del menú personalitzats", "show_custom_menu_links": "Mostrar enllaços del menú personalitzats",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Oculta biblioteques", "hide_libraries": "Oculta biblioteques",
"select_liraries_you_want_to_hide": "Seleccioneu les biblioteques que voleu ocultar de la pestanya Biblioteca i de les seccions de la pàgina d'inici.", "select_liraries_you_want_to_hide": "Seleccioneu les biblioteques que voleu ocultar de la pestanya Biblioteca i de les seccions de la pàgina d'inici.",
"disable_haptic_feedback": "Desactiva la resposta hàptica", "disable_haptic_feedback": "Desactiva la resposta hàptica",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Nombre màxim d'episodis de reproducció automàtica", "max_auto_play_episode_count": "Nombre màxim d'episodis de reproducció automàtica",
"disabled": "Desactivat" "disabled": "Desactivat"
}, },
"downloads": {
"downloads_title": "Descàrregues"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Mostra més sobre Marlin.", "read_more_about_marlin": "Mostra més sobre Marlin.",
"save_button": "Desa", "save_button": "Desa",
"toasts": { "toasts": {
"saved": "Desat", "saved": "Desat"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Suprimeix tots els fitxers descarregats", "delete_all_downloaded_files": "Suprimeix tots els fitxers descarregats",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Exporta registres", "export_logs": "Exporta registres",
"click_for_more_info": "Feu clic per obtenir més informació", "click_for_more_info": "Feu clic per obtenir més informació",
"level": "Nivell", "level": "Nivell",
"no_logs_available": "No hi ha registres disponibles", "no_logs_available": "No hi ha registres disponibles"
"delete_all_logs": "Suprimeix tots els registres"
}, },
"languages": { "languages": {
"title": "Idiomes", "title": "Idiomes",
@@ -490,15 +414,12 @@
"system": "Sistema" "system": "Sistema"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error en suprimir fitxers", "error_deleting_files": "Error en suprimir fitxers"
"background_downloads_enabled": "Descàrregues en segon pla activades",
"background_downloads_disabled": "Descàrregues en segon pla desactivades"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Descàrregues", "downloads_title": "Descàrregues",
"tvseries": "Sèries", "tvseries": "Sèries",
"movies": "Pel·lícules", "movies": "Pel·lícules",
"queue": "Cua",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "La cua i les descàrregues es perdran en reiniciar l'aplicació",
"no_items_in_queue": "No hi ha elements a la cua",
"no_downloaded_items": "No hi ha elements descarregats", "no_downloaded_items": "No hi ha elements descarregats",
"delete_all_movies_button": "Suprimeix totes les pel·lícules", "delete_all_movies_button": "Suprimeix totes les pel·lícules",
"delete_all_tvseries_button": "Suprimeix totes les sèries", "delete_all_tvseries_button": "Suprimeix totes les sèries",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "No s'han pogut suprimir totes les sèries", "failed_to_delete_all_tvseries": "No s'han pogut suprimir totes les sèries",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Descàrrega cancel·lada", "download_cancelled": "Descàrrega cancel·lada",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Descàrrega completada", "download_completed": "Descàrrega completada",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Ha fallat la descàrrega per a {{item}} - {{error}}", "download_failed_for_item": "Ha fallat la descàrrega per a {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Tots els fitxers, carpetes i treballs s'han suprimit correctament",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Ves a les descàrregues",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "No s'ha pogut crear un flux per a Chromecast", "could_not_create_stream_for_chromecast": "No s'ha pogut crear un flux per a Chromecast",
"message_from_server": "Missatge del servidor: {{message}}", "message_from_server": "Missatge del servidor: {{message}}",
"next_episode": "Episodi següent", "next_episode": "Episodi següent",
"refresh_tracks": "Actualitzar pistes",
"audio_tracks": "Pistes d'àudio:",
"playback_state": "Estat de reproducció:",
"index": "Índex:",
"continue_watching": "Continuar veient", "continue_watching": "Continuar veient",
"go_back": "Enrere", "go_back": "Enrere",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Mostra més", "show_more": "Mostra més",
"show_less": "Mostra menys", "show_less": "Mostra menys",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Nedávno přidané v {{libraryName}}", "recently_added_in": "Nedávno přidané v {{libraryName}}",
"suggested_movies": "Navrhované filmy", "suggested_movies": "Navrhované filmy",
"suggested_episodes": "Navrhované epizody",
"intro": { "intro": {
"welcome_to_streamyfin": "Vítejte v Streamyfin", "welcome_to_streamyfin": "Vítejte v Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Volný a Open-Source klient pro Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Volný a Open-Source klient pro Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Nic", "None": "Nic",
"OnlyForced": "Pouze vynucené" "OnlyForced": "Pouze vynucené"
}, },
"text_color": "Barva textu",
"background_color": "Barva pozadí",
"outline_color": "Barva obrysu",
"outline_thickness": "Obrys tloušťky",
"background_opacity": "Průhlednost pozadí",
"outline_opacity": "Průhlednost obrysu",
"bold_text": "Bold Text",
"colors": {
"Black": "Černý",
"Gray": "Šedá",
"Silver": "Stříbro",
"White": "Bílý",
"Maroon": "Maroon",
"Red": "Červená",
"Fuchsia": "Fuchsia",
"Yellow": "Žlutá",
"Olive": "Olivy",
"Green": "Zelená",
"Teal": "Modrozelený",
"Lime": "Světle zelená",
"Purple": "Fialová",
"Navy": "Námořní loď",
"Blue": "Modrá",
"Aqua": "Aqua"
},
"thickness": {
"None": "Nic",
"Thin": "Tenké",
"Normal": "Normální",
"Thick": "Tlustá"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Ostatní", "other_title": "Ostatní",
"video_orientation": "Orientace videa", "video_orientation": "Orientace videa",
@@ -351,13 +294,7 @@
"UNKNOWN": "Neznámý" "UNKNOWN": "Neznámý"
}, },
"safe_area_in_controls": "Bezpečná oblast v ovládání", "safe_area_in_controls": "Bezpečná oblast v ovládání",
"video_player": "Video přehrávač",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (experimentální + PiP)"
},
"show_custom_menu_links": "Zobrazit vlastní Menu odkazy", "show_custom_menu_links": "Zobrazit vlastní Menu odkazy",
"show_large_home_carousel": "Zobrazit velký přehled (beta)",
"hide_libraries": "Skrýt knihovny", "hide_libraries": "Skrýt knihovny",
"select_liraries_you_want_to_hide": "Vyberte knihovny, které chcete skrýt v záložce Knihovna a v sekcích domovské stránky.", "select_liraries_you_want_to_hide": "Vyberte knihovny, které chcete skrýt v záložce Knihovna a v sekcích domovské stránky.",
"disable_haptic_feedback": "Zakázat Haptickou zpětnou vazbu", "disable_haptic_feedback": "Zakázat Haptickou zpětnou vazbu",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maximální počet automatických přehrávání epizod", "max_auto_play_episode_count": "Maximální počet automatických přehrávání epizod",
"disabled": "Zakázáno" "disabled": "Zakázáno"
}, },
"downloads": {
"downloads_title": "Stahování"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Přečtěte si více o Marlinu.", "read_more_about_marlin": "Přečtěte si více o Marlinu.",
"save_button": "Uložit", "save_button": "Uložit",
"toasts": { "toasts": {
"saved": "Uloženo", "saved": "Uloženo"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Odstranit všechny stažené soubory", "delete_all_downloaded_files": "Odstranit všechny stažené soubory",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Exportovat protokoly", "export_logs": "Exportovat protokoly",
"click_for_more_info": "Klikněte pro více informací", "click_for_more_info": "Klikněte pro více informací",
"level": "Úrovně", "level": "Úrovně",
"no_logs_available": "Žádné protokoly nejsou k dispozici", "no_logs_available": "Žádné protokoly nejsou k dispozici"
"delete_all_logs": "Odstranit všechny logy"
}, },
"languages": { "languages": {
"title": "Jazyky", "title": "Jazyky",
@@ -490,15 +414,12 @@
"system": "Systém" "system": "Systém"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Chyba při mazání souborů", "error_deleting_files": "Chyba při mazání souborů"
"background_downloads_enabled": "Stahování na pozadí povoleno",
"background_downloads_disabled": "Stahování na pozadí zakázáno"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Stahování", "downloads_title": "Stahování",
"tvseries": "Televizní série", "tvseries": "Televizní série",
"movies": "Filmy", "movies": "Filmy",
"queue": "Fronta",
"other_media": "Ostatní média", "other_media": "Ostatní média",
"queue_hint": "Fronta a stahování budou ztraceny při restartu aplikace",
"no_items_in_queue": "Žádné položky ve frontě",
"no_downloaded_items": "Žádné stažené položky", "no_downloaded_items": "Žádné stažené položky",
"delete_all_movies_button": "Odstranit všechny filmy", "delete_all_movies_button": "Odstranit všechny filmy",
"delete_all_tvseries_button": "Odstranit všechny TV-série", "delete_all_tvseries_button": "Odstranit všechny TV-série",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Nepodařilo se odstranit všechny TV-série", "failed_to_delete_all_tvseries": "Nepodařilo se odstranit všechny TV-série",
"deleted_media_successfully": "Ostatní média úspěšně smazána!", "deleted_media_successfully": "Ostatní média úspěšně smazána!",
"failed_to_delete_media": "Nepodařilo se odstranit ostatní média", "failed_to_delete_media": "Nepodařilo se odstranit ostatní média",
"download_deleted": "Stahování smazáno",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Stahování nelze odstranit", "could_not_delete_download": "Stahování nelze odstranit",
"download_paused": "Stahování pozastaveno",
"could_not_pause_download": "Nelze pozastavit stahování",
"download_resumed": "Stahování obnoveno",
"could_not_resume_download": "Nelze pokračovat v stahování",
"download_completed": "Stahování dokončeno", "download_completed": "Stahování dokončeno",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Stahování se nezdařilo pro {{item}} - {{error}}", "download_failed_for_item": "Stahování se nezdařilo pro {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Všechny soubory, složky a úlohy byly úspěšně odstraněny",
"failed_to_clean_cache_directory": "Nepodařilo se vyčistit adresář mezipaměti",
"could_not_get_download_url_for_item": "Nelze získat URL pro stažení {{itemName}}", "could_not_get_download_url_for_item": "Nelze získat URL pro stažení {{itemName}}",
"go_to_downloads": "Přejít na stahování",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Hledat...", "search": "Hledat...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Nelze vytvořit stream pro Chromecast", "could_not_create_stream_for_chromecast": "Nelze vytvořit stream pro Chromecast",
"message_from_server": "Zpráva od serveru: {{message}}", "message_from_server": "Zpráva od serveru: {{message}}",
"next_episode": "Další epizoda", "next_episode": "Další epizoda",
"refresh_tracks": "Obnovit skladby",
"audio_tracks": "Zvukové stopy:",
"playback_state": "Stav přehrávání:",
"index": "Index:",
"continue_watching": "Pokračovat ve sledování", "continue_watching": "Pokračovat ve sledování",
"go_back": "Zpět", "go_back": "Zpět",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Zobrazit více", "show_more": "Zobrazit více",
"show_less": "Zobrazit méně", "show_less": "Zobrazit méně",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Senest tilføjet i {{libraryName}}", "recently_added_in": "Senest tilføjet i {{libraryName}}",
"suggested_movies": "Foreslåede film", "suggested_movies": "Foreslåede film",
"suggested_episodes": "Foreslåede episoder",
"intro": { "intro": {
"welcome_to_streamyfin": "Velkommen til Streamyfin", "welcome_to_streamyfin": "Velkommen til Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "En gratis og open-source klient til Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "En gratis og open-source klient til Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Ingen", "None": "Ingen",
"OnlyForced": "Kun tvungne undertekster" "OnlyForced": "Kun tvungne undertekster"
}, },
"text_color": "Tekst Farve",
"background_color": "Baggrunds Farve",
"outline_color": "Omrids Farve",
"outline_thickness": "Omrids Tykkelse",
"background_opacity": "Baggrunds Gennemsigtighed",
"outline_opacity": "Omrids Gennemsigtighed",
"bold_text": "Bold Text",
"colors": {
"Black": "Sort",
"Gray": "Grå",
"Silver": "Sølv",
"White": "Hvid",
"Maroon": "Maroon",
"Red": "Rød",
"Fuchsia": "Fuchsia",
"Yellow": "Gul",
"Olive": "Oliven",
"Green": "Grøn",
"Teal": "Grønblåt",
"Lime": "Limegrøn",
"Purple": "Lilla",
"Navy": "Flåden",
"Blue": "Blå",
"Aqua": "Aqua"
},
"thickness": {
"None": "Ingen",
"Thin": "Tynd",
"Normal": "Normal",
"Thick": "Tyk"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Andet", "other_title": "Andet",
"video_orientation": "Videoorientering", "video_orientation": "Videoorientering",
@@ -351,13 +294,7 @@
"UNKNOWN": "Ukendt" "UNKNOWN": "Ukendt"
}, },
"safe_area_in_controls": "Sikkert område i kontroller", "safe_area_in_controls": "Sikkert område i kontroller",
"video_player": "Videospiller",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimentel + PiP)"
},
"show_custom_menu_links": "Vis tilpassede menulinks", "show_custom_menu_links": "Vis tilpassede menulinks",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Skjul biblioteker", "hide_libraries": "Skjul biblioteker",
"select_liraries_you_want_to_hide": "Vælg de biblioteker, du ønsker at skjule fra fanen Bibliotek og startside sektionerne.", "select_liraries_you_want_to_hide": "Vælg de biblioteker, du ønsker at skjule fra fanen Bibliotek og startside sektionerne.",
"disable_haptic_feedback": "Deaktiver haptisk feedback", "disable_haptic_feedback": "Deaktiver haptisk feedback",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maks. Auto Afspil Episode Antal", "max_auto_play_episode_count": "Maks. Auto Afspil Episode Antal",
"disabled": "Deaktiveret" "disabled": "Deaktiveret"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Læs mere om Marlin.", "read_more_about_marlin": "Læs mere om Marlin.",
"save_button": "Gem", "save_button": "Gem",
"toasts": { "toasts": {
"saved": "Gemt", "saved": "Gemt"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Slet alle downloadede filer", "delete_all_downloaded_files": "Slet alle downloadede filer",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Eksporter logfiler", "export_logs": "Eksporter logfiler",
"click_for_more_info": "Klik for mere info", "click_for_more_info": "Klik for mere info",
"level": "Niveau", "level": "Niveau",
"no_logs_available": "Ingen logfiler tilgængelige", "no_logs_available": "Ingen logfiler tilgængelige"
"delete_all_logs": "Slet alle logfiler"
}, },
"languages": { "languages": {
"title": "Sprog", "title": "Sprog",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Fejl ved sletning af filer", "error_deleting_files": "Fejl ved sletning af filer"
"background_downloads_enabled": "Baggrundsdownloads aktiveret",
"background_downloads_disabled": "Baggrundsdownloads deaktiveret"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-serier", "tvseries": "TV-serier",
"movies": "Film", "movies": "Film",
"queue": "Kø",
"other_media": "Andre medier", "other_media": "Andre medier",
"queue_hint": "Kø og downloads vil gå tabt ved genstart af appen",
"no_items_in_queue": "Ingen elementer i køen",
"no_downloaded_items": "Ingen downloadede elementer", "no_downloaded_items": "Ingen downloadede elementer",
"delete_all_movies_button": "Slet alle film", "delete_all_movies_button": "Slet alle film",
"delete_all_tvseries_button": "Slet alle TV-serier", "delete_all_tvseries_button": "Slet alle TV-serier",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier", "failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier",
"deleted_media_successfully": "Slettede andre medier med succes!", "deleted_media_successfully": "Slettede andre medier med succes!",
"failed_to_delete_media": "Kunne ikke slette andre medier", "failed_to_delete_media": "Kunne ikke slette andre medier",
"download_deleted": "Download Slettet",
"download_cancelled": "Download afbrudt", "download_cancelled": "Download afbrudt",
"could_not_delete_download": "Kunne Ikke Slette Download", "could_not_delete_download": "Kunne Ikke Slette Download",
"download_paused": "Download Pauset",
"could_not_pause_download": "Kunne Ikke Pause Download",
"download_resumed": "Download Genoprettet",
"could_not_resume_download": "Kunne Ikke Genoptage Download",
"download_completed": "Download fuldført", "download_completed": "Download fuldført",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Download mislykkedes for {{item}} - {{error}}", "download_failed_for_item": "Download mislykkedes for {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobs blev slettet med succes",
"failed_to_clean_cache_directory": "Kunne ikke rense cache-mappe",
"could_not_get_download_url_for_item": "Kunne ikke hente download URL til {{itemName}}", "could_not_get_download_url_for_item": "Kunne ikke hente download URL til {{itemName}}",
"go_to_downloads": "Gå til downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Søg...", "search": "Søg...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Kunne ikke oprette en stream til Chromecast", "could_not_create_stream_for_chromecast": "Kunne ikke oprette en stream til Chromecast",
"message_from_server": "Besked fra server: {{message}}", "message_from_server": "Besked fra server: {{message}}",
"next_episode": "Næste episode", "next_episode": "Næste episode",
"refresh_tracks": "Opdater spor",
"audio_tracks": "Lydspor:",
"playback_state": "Afspilningstilstand:",
"index": "Indeks:",
"continue_watching": "Fortsæt med at se", "continue_watching": "Fortsæt med at se",
"go_back": "Gå Tilbage", "go_back": "Gå Tilbage",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Vis mere", "show_more": "Vis mere",
"show_less": "Vis mindre", "show_less": "Vis mindre",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"", "continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"",
"recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}",
"suggested_movies": "Empfohlene Filme", "suggested_movies": "Empfohlene Filme",
"suggested_episodes": "Empfohlene Episoden",
"intro": { "intro": {
"welcome_to_streamyfin": "Willkommen bei Streamyfin", "welcome_to_streamyfin": "Willkommen bei Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Keine", "None": "Keine",
"OnlyForced": "Nur erzwungene" "OnlyForced": "Nur erzwungene"
}, },
"text_color": "Textfarbe",
"background_color": "Hintergrundfarbe",
"outline_color": "Konturfarbe",
"outline_thickness": "Konturdicke",
"background_opacity": "Hintergrundtransparenz",
"outline_opacity": "Konturtransparenz",
"bold_text": "Fettgedruckter Text",
"colors": {
"Black": "Schwarz",
"Gray": "Grau",
"Silver": "Silber",
"White": "Weiß",
"Maroon": "Rotbraun",
"Red": "Rot",
"Fuchsia": "Magenta",
"Yellow": "Gelb",
"Olive": "Olivgrün",
"Green": "Grün",
"Teal": "Türkis",
"Lime": "Hellgrün",
"Purple": "Lila",
"Navy": "Marineblau",
"Blue": "Blau",
"Aqua": "Himmelblau"
},
"thickness": {
"None": "Keine",
"Thin": "Dünn",
"Normal": "Normal",
"Thick": "Dick"
},
"subtitle_color": "Untertitelfarbe",
"subtitle_background_color": "Hintergrundfarbe",
"subtitle_font": "Untertitel-Schriftart",
"ksplayer_title": "KSPlayer Einstellungen",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Untertitel-Einstellungen",
"hint": "Anpassen des Untertitel-Erscheinungsbildes für VLC. Änderungen werden bei der nächsten Wiedergabe übernommen.",
"text_color": "Schriftfarbe",
"background_color": "Hintergrundfarbe",
"background_opacity": "Hintergrundtransparenz",
"outline_color": "Konturfarbe",
"outline_opacity": "Konturtransparenz",
"outline_thickness": "Konturdicke",
"bold": "Fettgedruckter Text",
"margin": "Unterer Abstand"
},
"video_player": {
"title": "Videoplayer",
"video_player": "Videoplayer",
"video_player_description": "Videoplayer auf iOS auswählen.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Sonstiges", "other_title": "Sonstiges",
"video_orientation": "Videoausrichtung", "video_orientation": "Videoausrichtung",
@@ -351,13 +294,7 @@
"UNKNOWN": "Unbekannt" "UNKNOWN": "Unbekannt"
}, },
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen", "safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
"video_player": "Videoplayer",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimentell + PiP)"
},
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
"show_large_home_carousel": "Zeige große Startseiten-Übersicht (Beta)",
"hide_libraries": "Bibliotheken ausblenden", "hide_libraries": "Bibliotheken ausblenden",
"select_liraries_you_want_to_hide": "Bibliotheken auswählen die aus dem Bibliothekstab und der Startseite ausgeblendet werden sollen.", "select_liraries_you_want_to_hide": "Bibliotheken auswählen die aus dem Bibliothekstab und der Startseite ausgeblendet werden sollen.",
"disable_haptic_feedback": "Haptisches Feedback deaktivieren", "disable_haptic_feedback": "Haptisches Feedback deaktivieren",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maximale automatisch abzuspielende Episodenanzahl", "max_auto_play_episode_count": "Maximale automatisch abzuspielende Episodenanzahl",
"disabled": "Deaktiviert" "disabled": "Deaktiviert"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Musik", "title": "Musik",
"playback_title": "Wiedergabe", "playback_title": "Wiedergabe",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Erfahre mehr über Marlin.", "read_more_about_marlin": "Erfahre mehr über Marlin.",
"save_button": "Speichern", "save_button": "Speichern",
"toasts": { "toasts": {
"saved": "Gespeichert", "saved": "Gespeichert"
"refreshed": "Einstellungen vom Server aktualisiert" }
},
"refresh_from_server": "Einstellungen vom Server aktualisieren"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Streamystats aktivieren",
"disable_streamystats": "Streamystats deaktivieren", "disable_streamystats": "Streamystats deaktivieren",
"enable_search": "Zum Suchen verwenden", "enable_search": "Zum Suchen verwenden",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "URL für den Streamystats-Server eingeben.", "streamystats_search_hint": "URL für den Streamystats-Server eingeben.",
"read_more_about_streamystats": "Mehr über Streamystats erfahren.", "read_more_about_streamystats": "Mehr über Streamystats erfahren.",
"save_button": "Speichern",
"save": "Gespeichert", "save": "Gespeichert",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Startseitenbereiche",
"enable_movie_recommendations": "Filmempfehlungen", "enable_movie_recommendations": "Filmempfehlungen",
"enable_series_recommendations": "Serienempfehlungen", "enable_series_recommendations": "Serienempfehlungen",
"enable_promoted_watchlists": "Empfohlene Merklisten", "enable_promoted_watchlists": "Empfohlene Merklisten",
@@ -445,8 +374,7 @@
"refresh_from_server": "Einstellungen vom Server aktualisieren" "refresh_from_server": "Einstellungen vom Server aktualisieren"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Merklisten-Integration aktivieren", "watchlist_enabler": "Merklisten-Integration aktivieren"
"watchlist_button": "Merklisten-Integration umschalten"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Alle heruntergeladenen Dateien löschen", "delete_all_downloaded_files": "Alle heruntergeladenen Dateien löschen",
"music_cache_title": "Musik-Cache", "music_cache_title": "Musik-Cache",
"music_cache_description": "Beim Anhören Titel automatisch in den Cache laden um bessere Wiedergabe und Offline-Wiedergabe zu ermöglichen", "music_cache_description": "Beim Anhören Titel automatisch in den Cache laden um bessere Wiedergabe und Offline-Wiedergabe zu ermöglichen",
"enable_music_cache": "Musik-Cache aktivieren",
"clear_music_cache": "Musik-Cache leeren", "clear_music_cache": "Musik-Cache leeren",
"music_cache_size": "{{size}} gechached", "music_cache_size": "{{size}} gechached",
"music_cache_cleared": "Musik-Cache geleert", "music_cache_cleared": "Musik-Cache geleert",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Logs exportieren", "export_logs": "Logs exportieren",
"click_for_more_info": "Für mehr Informationen klicken", "click_for_more_info": "Für mehr Informationen klicken",
"level": "Level", "level": "Level",
"no_logs_available": "Keine Logs verfügbar", "no_logs_available": "Keine Logs verfügbar"
"delete_all_logs": "Alle Logs löschen"
}, },
"languages": { "languages": {
"title": "Sprachen", "title": "Sprachen",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Fehler beim Löschen von Dateien", "error_deleting_files": "Fehler beim Löschen von Dateien"
"background_downloads_enabled": "Hintergrunddownloads aktiviert",
"background_downloads_disabled": "Hintergrunddownloads deaktiviert"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "Serien", "tvseries": "Serien",
"movies": "Filme", "movies": "Filme",
"queue": "Warteschlange",
"other_media": "Andere Medien", "other_media": "Andere Medien",
"queue_hint": "Warteschlange und aktive Downloads gehen verloren wenn die App neu gestartet wird",
"no_items_in_queue": "Keine Elemente in der Warteschlange",
"no_downloaded_items": "Keine heruntergeladenen Elemente", "no_downloaded_items": "Keine heruntergeladenen Elemente",
"delete_all_movies_button": "Alle Filme löschen", "delete_all_movies_button": "Alle Filme löschen",
"delete_all_tvseries_button": "Alle Serien löschen", "delete_all_tvseries_button": "Alle Serien löschen",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Fehler beim Löschen aller Serien", "failed_to_delete_all_tvseries": "Fehler beim Löschen aller Serien",
"deleted_media_successfully": "Andere Medien erfolgreich gelöscht!", "deleted_media_successfully": "Andere Medien erfolgreich gelöscht!",
"failed_to_delete_media": "Fehler beim Löschen anderer Medien", "failed_to_delete_media": "Fehler beim Löschen anderer Medien",
"download_deleted": "Download gelöscht",
"download_cancelled": "Download abgebrochen", "download_cancelled": "Download abgebrochen",
"could_not_delete_download": "Download konnte nicht gelöscht werden", "could_not_delete_download": "Download konnte nicht gelöscht werden",
"download_paused": "Download pausiert",
"could_not_pause_download": "Download konnte nicht angehalten werden",
"download_resumed": "Download fortgesetzt",
"could_not_resume_download": "Download konnte nicht fortgesetzt werden",
"download_completed": "Download abgeschlossen", "download_completed": "Download abgeschlossen",
"download_failed": "Download fehlgeschlagen", "download_failed": "Download fehlgeschlagen",
"download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} Lädt", "item_already_downloading": "{{item}} Lädt",
"all_files_deleted": "Alle Downloads gelöscht", "all_files_deleted": "Alle Downloads gelöscht",
"files_deleted_by_type": "{{count}} {{type}} gelöscht", "files_deleted_by_type": "{{count}} {{type}} gelöscht",
"all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht",
"failed_to_clean_cache_directory": "Fehler beim Bereinigen des Cache-Verzeichnisses",
"could_not_get_download_url_for_item": "Download-URL für {{itemName}} konnte nicht geladen werden", "could_not_get_download_url_for_item": "Download-URL für {{itemName}} konnte nicht geladen werden",
"go_to_downloads": "Zu Downloads gehen",
"file_deleted": "{{item}} gelöscht" "file_deleted": "{{item}} gelöscht"
} }
} }
@@ -583,16 +493,13 @@
"none": "Keine", "none": "Keine",
"track": "Spur", "track": "Spur",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"stop": "Stop",
"delete": "Löschen", "delete": "Löschen",
"ok": "OK", "ok": "OK",
"remove": "Entfernen", "remove": "Entfernen",
"next": "Weiter",
"back": "Zurück", "back": "Zurück",
"continue": "Fortsetzen", "continue": "Fortsetzen",
"verifying": "Verifiziere...", "verifying": "Verifiziere...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Suchen...", "search": "Suchen...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen",
"message_from_server": "Nachricht vom Server: {{message}}", "message_from_server": "Nachricht vom Server: {{message}}",
"next_episode": "Nächste Episode", "next_episode": "Nächste Episode",
"refresh_tracks": "Spuren aktualisieren",
"audio_tracks": "Audiospuren:",
"playback_state": "Wiedergabestatus:",
"index": "Index:",
"continue_watching": "Fortsetzen", "continue_watching": "Fortsetzen",
"go_back": "Zurück", "go_back": "Zurück",
"downloaded_file_title": "Diese Datei wurde bereits heruntergeladen", "downloaded_file_title": "Diese Datei wurde bereits heruntergeladen",
@@ -761,7 +664,6 @@
"show_more": "Mehr anzeigen", "show_more": "Mehr anzeigen",
"show_less": "Weniger anzeigen", "show_less": "Weniger anzeigen",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "Titel" "tracks": "Titel"
}, },
"filters": {
"all": "Alle"
},
"recently_added": "Kürzlich hinzugefügt", "recently_added": "Kürzlich hinzugefügt",
"recently_played": "Vor kurzem gehört", "recently_played": "Vor kurzem gehört",
"frequently_played": "Oft gehört", "frequently_played": "Oft gehört",
"explore": "Entdecken",
"top_tracks": "Top-Titel", "top_tracks": "Top-Titel",
"play": "Abspielen", "play": "Abspielen",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}", "recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}",
"suggested_movies": "Προτεινόμενες Ταινίες", "suggested_movies": "Προτεινόμενες Ταινίες",
"suggested_episodes": "Προτεινόμενα Επεισόδια",
"intro": { "intro": {
"welcome_to_streamyfin": "Καλώς ήρθατε στο Streamyfin", "welcome_to_streamyfin": "Καλώς ήρθατε στο Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Ένας ελεύθερος και ανοιχτού κώδικα πελάτης για τη ζελυφίνη.", "a_free_and_open_source_client_for_jellyfin": "Ένας ελεύθερος και ανοιχτού κώδικα πελάτης για τη ζελυφίνη.",
@@ -261,43 +260,6 @@
"None": "Κανένα", "None": "Κανένα",
"OnlyForced": "Μόνο" "OnlyForced": "Μόνο"
}, },
"text_color": "Χρώμα Κειμένου",
"background_color": "Χρώμα Φόντου",
"outline_color": "Χρώμα Περιγράμματος",
"outline_thickness": "Πάχος Περιγράμματος",
"background_opacity": "Αδιαφάνεια Φόντου",
"outline_opacity": "Αδιαφάνεια Περιγράμματος",
"bold_text": "Bold Text",
"colors": {
"Black": "Μαύρο",
"Gray": "Γκρι",
"Silver": "Ασημένιο",
"White": "Λευκό",
"Maroon": "Μαρώ",
"Red": "Κόκκινο",
"Fuchsia": "Fuchsia",
"Yellow": "Κίτρινο",
"Olive": "Ελιές",
"Green": "Πράσινο",
"Teal": "Τιρκουάζ",
"Lime": "Άσβεστος",
"Purple": "Μωβ",
"Navy": "Ναυτικό",
"Blue": "Μπλε",
"Aqua": "Νερό"
},
"thickness": {
"None": "Κανένα",
"Thin": "Λεπτό",
"Normal": "Κανονικό",
"Thick": "Παχύ"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Άλλο", "other_title": "Άλλο",
"video_orientation": "Προσανατολισμός Βίντεο", "video_orientation": "Προσανατολισμός Βίντεο",
@@ -351,13 +294,7 @@
"UNKNOWN": "Άγνωστο" "UNKNOWN": "Άγνωστο"
}, },
"safe_area_in_controls": "Ασφαλής περιοχή σε χειριστήρια", "safe_area_in_controls": "Ασφαλής περιοχή σε χειριστήρια",
"video_player": "Αναπαραγωγέας Βίντεο",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Πειραματική + PiP)"
},
"show_custom_menu_links": "Εμφάνιση Προσαρμοσμένων Συνδέσμων Μενού", "show_custom_menu_links": "Εμφάνιση Προσαρμοσμένων Συνδέσμων Μενού",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Απόκρυψη Βιβλιοθηκών", "hide_libraries": "Απόκρυψη Βιβλιοθηκών",
"select_liraries_you_want_to_hide": "Επιλέξτε τις βιβλιοθήκες που θέλετε να αποκρύψετε από την καρτέλα της Βιβλιοθήκης και τις ενότητες της αρχικής σελίδας.", "select_liraries_you_want_to_hide": "Επιλέξτε τις βιβλιοθήκες που θέλετε να αποκρύψετε από την καρτέλα της Βιβλιοθήκης και τις ενότητες της αρχικής σελίδας.",
"disable_haptic_feedback": "Απενεργοποίηση Απτικής Ανατροφοδότησης", "disable_haptic_feedback": "Απενεργοποίηση Απτικής Ανατροφοδότησης",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Μέγιστο Πλήθος Επεισόδιο Αυτόματου Παιχνιδιού", "max_auto_play_episode_count": "Μέγιστο Πλήθος Επεισόδιο Αυτόματου Παιχνιδιού",
"disabled": "Απενεργοποιημένο" "disabled": "Απενεργοποιημένο"
}, },
"downloads": {
"downloads_title": "Λήψεις"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Διαβάστε Περισσότερα Σχετικά Με Marlin.", "read_more_about_marlin": "Διαβάστε Περισσότερα Σχετικά Με Marlin.",
"save_button": "Αποθήκευση", "save_button": "Αποθήκευση",
"toasts": { "toasts": {
"saved": "Αποθηκεύτηκε", "saved": "Αποθηκεύτηκε"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Διαγραφή Όλων Των Ληφθέντων Αρχείων", "delete_all_downloaded_files": "Διαγραφή Όλων Των Ληφθέντων Αρχείων",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Εξαγωγή Αρχείων Καταγραφής", "export_logs": "Εξαγωγή Αρχείων Καταγραφής",
"click_for_more_info": "Κάντε κλικ για περισσότερες πληροφορίες", "click_for_more_info": "Κάντε κλικ για περισσότερες πληροφορίες",
"level": "Επίπεδο", "level": "Επίπεδο",
"no_logs_available": "Δεν Υπάρχουν Διαθέσιμα Αρχεία Καταγραφής", "no_logs_available": "Δεν Υπάρχουν Διαθέσιμα Αρχεία Καταγραφής"
"delete_all_logs": "Διαγραφή Όλων Των Καταγραφών"
}, },
"languages": { "languages": {
"title": "Γλώσσες", "title": "Γλώσσες",
@@ -490,15 +414,12 @@
"system": "Σύστημα" "system": "Σύστημα"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Σφάλμα Διαγραφής Αρχείων", "error_deleting_files": "Σφάλμα Διαγραφής Αρχείων"
"background_downloads_enabled": "Οι λήψεις στο παρασκήνιο ενεργοποιήθηκαν",
"background_downloads_disabled": "Οι λήψεις παρασκηνίου απενεργοποιήθηκαν"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Λήψεις", "downloads_title": "Λήψεις",
"tvseries": "Τηλεόραση-Σειρά", "tvseries": "Τηλεόραση-Σειρά",
"movies": "Ταινίες", "movies": "Ταινίες",
"queue": "Ουρά",
"other_media": "Άλλα μέσα", "other_media": "Άλλα μέσα",
"queue_hint": "Ουρά και λήψεις θα χαθούν κατά την επανεκκίνηση της εφαρμογής",
"no_items_in_queue": "Δεν υπάρχουν αντικείμενα στην ουρά",
"no_downloaded_items": "Δεν Έχουν Ληφθεί Αντικείμενα", "no_downloaded_items": "Δεν Έχουν Ληφθεί Αντικείμενα",
"delete_all_movies_button": "Διαγραφή Όλων Των Ταινιών", "delete_all_movies_button": "Διαγραφή Όλων Των Ταινιών",
"delete_all_tvseries_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών", "delete_all_tvseries_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Αποτυχία διαγραφής Όλων των TV-Series", "failed_to_delete_all_tvseries": "Αποτυχία διαγραφής Όλων των TV-Series",
"deleted_media_successfully": "Διαγράφηκε άλλο μέσο επιτυχώς!", "deleted_media_successfully": "Διαγράφηκε άλλο μέσο επιτυχώς!",
"failed_to_delete_media": "Αποτυχία διαγραφής άλλων πολυμέσων", "failed_to_delete_media": "Αποτυχία διαγραφής άλλων πολυμέσων",
"download_deleted": "Η Λήψη Διαγράφηκε",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Αδυναμία Διαγραφής Λήψης", "could_not_delete_download": "Αδυναμία Διαγραφής Λήψης",
"download_paused": "Λήψη Σε Παύση",
"could_not_pause_download": "Αδυναμία Παύσης Λήψης",
"download_resumed": "Συνέχιση Λήψης",
"could_not_resume_download": "Αδυναμία Συνέχισης Λήψης",
"download_completed": "Η Λήψη Ολοκληρώθηκε", "download_completed": "Η Λήψη Ολοκληρώθηκε",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Η λήψη απέτυχε για το {{item}} - {{error}}", "download_failed_for_item": "Η λήψη απέτυχε για το {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Όλα τα αρχεία, οι φάκελοι και οι εργασίες διαγράφηκαν με επιτυχία",
"failed_to_clean_cache_directory": "Αποτυχία καθαρισμού φακέλου προσωρινής μνήμης",
"could_not_get_download_url_for_item": "Αδυναμία λήψης του URL λήψης για το {{itemName}}", "could_not_get_download_url_for_item": "Αδυναμία λήψης του URL λήψης για το {{itemName}}",
"go_to_downloads": "Μετάβαση στις λήψεις",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Αναζήτηση...", "search": "Αναζήτηση...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Αδυναμία δημιουργίας ροής για το Chromecast", "could_not_create_stream_for_chromecast": "Αδυναμία δημιουργίας ροής για το Chromecast",
"message_from_server": "Μήνυμα από το διακομιστή: {{message}}", "message_from_server": "Μήνυμα από το διακομιστή: {{message}}",
"next_episode": "Επόμενο Επεισόδιο", "next_episode": "Επόμενο Επεισόδιο",
"refresh_tracks": "Ανανέωση Κομματιών",
"audio_tracks": "Κομμάτια Ήχου:",
"playback_state": "Κατάσταση Αναπαραγωγής:",
"index": "Δείκτης:",
"continue_watching": "Συνέχεια Παρακολούθησης", "continue_watching": "Συνέχεια Παρακολούθησης",
"go_back": "Μετάβαση Πίσω", "go_back": "Μετάβαση Πίσω",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Εμφάνιση Περισσότερων", "show_more": "Εμφάνιση Περισσότερων",
"show_less": "Εμφάνιση Λιγότερων", "show_less": "Εμφάνιση Λιγότερων",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -25,113 +25,8 @@
"there_is_a_server_error": "There is a server error", "there_is_a_server_error": "There is a server error",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
"too_old_server_text": "Unsupported Jellyfin Server Discovered", "too_old_server_text": "Unsupported Jellyfin Server Discovered",
"too_old_server_description": "Please update Jellyfin to the latest version" "too_old_server_description": "Please update Jellyfin to the latest version",
}, "login": "Log In"
"player": {
"skip_intro": "Skip Intro",
"live": "LIVE",
"mpv_player_title": "MPV Player",
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded",
"skip_outro": "Skip Outro",
"skip_recap": "Skip Recap",
"skip_commercial": "Skip Commercial",
"skip_preview": "Skip Preview",
"error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL",
"an_error_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes",
"downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel",
"up_next": "Up next",
"next_episode_in": "Next episode in {{seconds}}s",
"play_now": "Play now",
"cancel": "Cancel"
},
"casting_player": {
"buffering": "Buffering...",
"changing_audio": "Changing audio...",
"changing_subtitles": "Changing subtitles...",
"season_episode_format": "Season {{season}} • Episode {{episode}}",
"connecting": "Connecting to Chromecast...",
"unknown_device": "Unknown Device",
"ending_at": "Ending at {{time}}",
"unknown": "Unknown",
"connected": "Connected",
"volume": "Volume",
"muted": "Muted",
"disconnect": "Disconnect",
"stop_casting": "Stop Casting",
"disconnecting": "Disconnecting...",
"chromecast": "Chromecast",
"device_name": "Device Name",
"playback_settings": "Playback Settings",
"version": "Version",
"stop": "Stop",
"quality": "Quality",
"audio": "Audio",
"subtitles": "Subtitles",
"none": "None",
"playback_speed": "Playback Speed",
"normal": "Normal",
"episodes": "Episodes",
"season": "Season {{number}}",
"minutes_short": "min",
"episode_label": "Episode {{number}}",
"forced": "Forced",
"device": "Device",
"cancel": "Cancel",
"connection_quality": {
"excellent": "Excellent",
"good": "Good",
"fair": "Fair",
"poor": "Poor",
"disconnected": "Disconnected"
},
"error_title": "Chromecast Error",
"error_description": "Something went wrong with the cast session",
"retry": "Try Again",
"critical_error_title": "Multiple Errors Detected",
"critical_error_description": "Chromecast encountered multiple errors. Please disconnect and try casting again.",
"track_changed": "Track changed successfully",
"audio_track_changed": "Audio track changed",
"subtitle_track_changed": "Subtitle track changed",
"seeking": "Seeking...",
"seeking_error": "Failed to seek",
"load_failed": "Failed to load media",
"load_retry": "Retrying media load..."
}, },
"server": { "server": {
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
@@ -206,7 +101,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Recently Added in {{libraryName}}", "recently_added_in": "Recently Added in {{libraryName}}",
"suggested_movies": "Suggested Movies", "suggested_movies": "Suggested Movies",
"suggested_episodes": "Suggested Episodes",
"intro": { "intro": {
"welcome_to_streamyfin": "Welcome to Streamyfin", "welcome_to_streamyfin": "Welcome to Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
@@ -367,43 +261,6 @@
"None": "None", "None": "None",
"OnlyForced": "OnlyForced" "OnlyForced": "OnlyForced"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "None",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -421,25 +278,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Other", "other_title": "Other",
"video_orientation": "Video Orientation", "video_orientation": "Video Orientation",
@@ -457,13 +295,7 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "Safe Area in Controls", "safe_area_in_controls": "Safe Area in Controls",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Show Custom Menu Links", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries", "hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback", "disable_haptic_feedback": "Disable Haptic Feedback",
@@ -471,28 +303,8 @@
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Auto-play Next Episode", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"segment_skip_settings": "Segment Skip Settings",
"segment_skip_settings_description": "Configure skip behavior for intros, credits, and other segments",
"skip_intro": "Skip Intro",
"skip_intro_description": "Action when intro segment is detected",
"skip_outro": "Skip Outro/Credits",
"skip_outro_description": "Action when outro/credits segment is detected",
"skip_recap": "Skip Recap",
"skip_recap_description": "Action when recap segment is detected",
"skip_commercial": "Skip Commercial",
"skip_commercial_description": "Action when commercial segment is detected",
"skip_preview": "Skip Preview",
"skip_preview_description": "Action when preview segment is detected",
"segment_skip_none": "None",
"segment_skip_ask": "Show Skip Button",
"segment_skip_auto": "Auto Skip",
"autoplay_countdown_seconds": "Player countdown (seconds)",
"cast_autoplay_countdown_seconds": "Chromecast countdown (seconds)",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -536,23 +348,18 @@
"read_more_about_marlin": "Read More About Marlin.", "read_more_about_marlin": "Read More About Marlin.",
"save_button": "Save", "save_button": "Save",
"toasts": { "toasts": {
"saved": "Saved", "saved": "Saved"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -568,8 +375,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -580,7 +386,6 @@
"delete_all_downloaded_files": "Delete All Downloaded Files", "delete_all_downloaded_files": "Delete All Downloaded Files",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -590,8 +395,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -604,8 +407,7 @@
"export_logs": "Export Logs", "export_logs": "Export Logs",
"click_for_more_info": "Click for More Info", "click_for_more_info": "Click for More Info",
"level": "Level", "level": "Level",
"no_logs_available": "No Logs Available", "no_logs_available": "No Logs Available"
"delete_all_logs": "Delete All Logs"
}, },
"languages": { "languages": {
"title": "Languages", "title": "Languages",
@@ -613,15 +415,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -631,6 +430,10 @@
"4_hours": "4 hours", "4_hours": "4 hours",
"24_hours": "24 hours" "24_hours": "24 hours"
} }
},
"dashboard": {
"title": "Dashboard",
"sessions_title": "Sessions"
} }
}, },
"sessions": { "sessions": {
@@ -641,10 +444,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-Series", "tvseries": "TV-Series",
"movies": "Movies", "movies": "Movies",
"queue": "Queue",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Queue and downloads will be lost on app restart",
"no_items_in_queue": "No Items in Queue",
"no_downloaded_items": "No Downloaded Items", "no_downloaded_items": "No Downloaded Items",
"delete_all_movies_button": "Delete All Movies", "delete_all_movies_button": "Delete All Movies",
"delete_all_tvseries_button": "Delete All TV-Series", "delete_all_tvseries_button": "Delete All TV-Series",
@@ -669,13 +469,8 @@
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Download Completed", "download_completed": "Download Completed",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}", "download_failed_for_item": "Download failed for {{item}} - {{error}}",
@@ -685,10 +480,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -706,16 +498,17 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login",
"refresh": "Refresh" "episodes": "Episodes",
"movies": "Movies",
"loading": "Loading…",
"seeAll": "See all"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -804,6 +597,47 @@
"custom_links": { "custom_links": {
"no_links": "No Links" "no_links": "No Links"
}, },
"player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL",
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode",
"continue_watching": "Continue Watching",
"go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes",
"downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel",
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded",
"missing_parameters": "Missing playback parameters"
},
"chapters": { "chapters": {
"title": "Chapters", "title": "Chapters",
"chapter_number": "Chapter {{number}}", "chapter_number": "Chapter {{number}}",
@@ -840,7 +674,6 @@
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -863,7 +696,8 @@
"resume_playback": "Resume Playback", "resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?", "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": "Continue from {{time}}" "continue_from": "Continue from {{time}}",
"no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Next", "next": "Next",
@@ -967,13 +801,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1107,7 +937,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "Sekva", "next_up": "Sekva",
"recently_added_in": "Ĵus Aldonita en {{libraryName}}", "recently_added_in": "Ĵus Aldonita en {{libraryName}}",
"suggested_movies": "Sugestitaj Filmoj", "suggested_movies": "Sugestitaj Filmoj",
"suggested_episodes": "Sugestitaj Epizodoj",
"intro": { "intro": {
"welcome_to_streamyfin": "Bonvenon al Streamyfin", "welcome_to_streamyfin": "Bonvenon al Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Senpaga kaj malfermfonta kliento por Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Senpaga kaj malfermfonta kliento por Jellyfin.",
@@ -128,11 +127,6 @@
"UNKNOWN": "Nekonata" "UNKNOWN": "Nekonata"
}, },
"safe_area_in_controls": "Sekura areo en kontroloj", "safe_area_in_controls": "Sekura areo en kontroloj",
"video_player": "Video-ludilo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimenta + PiP)"
},
"show_custom_menu_links": "Montri Proprajn Menuajn Ligilojn", "show_custom_menu_links": "Montri Proprajn Menuajn Ligilojn",
"hide_libraries": "Kaŝi Bibliotekojn", "hide_libraries": "Kaŝi Bibliotekojn",
"select_liraries_you_want_to_hide": "Elektu la bibliotekojn, kiujn vi volas kaŝi de la Biblioteka langeto kaj hejmpaĝaj sekcioj.", "select_liraries_you_want_to_hide": "Elektu la bibliotekojn, kiujn vi volas kaŝi de la Biblioteka langeto kaj hejmpaĝaj sekcioj.",
@@ -140,7 +134,6 @@
"default_quality": "Defaŭlta kvalito" "default_quality": "Defaŭlta kvalito"
}, },
"downloads": { "downloads": {
"downloads_title": "Elŝutoj",
"optimized_versions_server": "Optimumigitaj versioj servilo", "optimized_versions_server": "Optimumigitaj versioj servilo",
"save_button": "Konservi", "save_button": "Konservi",
"optimized_server": "Optimumigita Servilo", "optimized_server": "Optimumigita Servilo",
@@ -205,8 +198,7 @@
"export_logs": "Eksporti protokolojn", "export_logs": "Eksporti protokolojn",
"click_for_more_info": "Klaku por pli da informoj", "click_for_more_info": "Klaku por pli da informoj",
"level": "Nivelo", "level": "Nivelo",
"no_logs_available": "Neniuj protokoloj disponeblaj", "no_logs_available": "Neniuj protokoloj disponeblaj"
"delete_all_logs": "Forigi ĉiujn protokolojn"
}, },
"languages": { "languages": {
"title": "Lingvoj", "title": "Lingvoj",
@@ -216,8 +208,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "Eraro forigante dosierojn", "error_deleting_files": "Eraro forigante dosierojn",
"background_downloads_enabled": "Fonaj elŝutoj ebligitaj",
"background_downloads_disabled": "Fonaj elŝutoj malŝaltitaj",
"connected": "Konektita", "connected": "Konektita",
"could_not_connect": "Ne povis konekti", "could_not_connect": "Ne povis konekti",
"invalid_url": "Nevalida URL" "invalid_url": "Nevalida URL"
@@ -231,9 +221,6 @@
"downloads_title": "Elŝutoj", "downloads_title": "Elŝutoj",
"tvseries": "Televidaj serioj", "tvseries": "Televidaj serioj",
"movies": "Filmoj", "movies": "Filmoj",
"queue": "Vico",
"queue_hint": "Vico kaj elŝutoj perdiĝos ĉe aplikaĵa rekomenco",
"no_items_in_queue": "Neniuj eroj en vico",
"no_downloaded_items": "Neniuj elŝutitaj eroj", "no_downloaded_items": "Neniuj elŝutitaj eroj",
"delete_all_movies_button": "Forigi ĉiujn Filmojn", "delete_all_movies_button": "Forigi ĉiujn Filmojn",
"delete_all_tvseries_button": "Forigi ĉiujn Televidajn Seriojn", "delete_all_tvseries_button": "Forigi ĉiujn Televidajn Seriojn",
@@ -269,9 +256,7 @@
"no_response_received_from_server": "Neniu respondo ricevita de la servilo", "no_response_received_from_server": "Neniu respondo ricevita de la servilo",
"error_setting_up_the_request": "Eraro starigante la peton", "error_setting_up_the_request": "Eraro starigante la peton",
"failed_to_start_download_for_item_unexpected_error": "Malsukcesis komenci elŝutadon por {{item}}: Neatendita eraro", "failed_to_start_download_for_item_unexpected_error": "Malsukcesis komenci elŝutadon por {{item}}: Neatendita eraro",
"all_files_folders_and_jobs_deleted_successfully": "Ĉiuj dosieroj, dosierujoj kaj taskoj sukcese forigitaj", "an_error_occured_while_deleting_files_and_jobs": "Eraro okazis dum forigo de dosieroj kaj taskoj"
"an_error_occured_while_deleting_files_and_jobs": "Eraro okazis dum forigo de dosieroj kaj taskoj",
"go_to_downloads": "Iri al elŝutoj"
} }
} }
}, },
@@ -365,12 +350,8 @@
"video_has_finished_playing": "Video finis ludi!", "video_has_finished_playing": "Video finis ludi!",
"no_video_source": "Neniu video-fonto...", "no_video_source": "Neniu video-fonto...",
"next_episode": "Sekva Epizodo", "next_episode": "Sekva Epizodo",
"refresh_tracks": "Refreŝigi Trakojn",
"subtitle_tracks": "Subtekstaj Trakoj:", "subtitle_tracks": "Subtekstaj Trakoj:",
"audio_tracks": "Aŭdiaj Trakoj:", "no_data_available": "Neniuj datumoj disponeblaj"
"playback_state": "Ludada Stato:",
"no_data_available": "Neniuj datumoj disponeblaj",
"index": "Indekso:"
}, },
"item_card": { "item_card": {
"next_up": "Sekva", "next_up": "Sekva",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continuar y siguiente", "continue_and_next_up": "Continuar y siguiente",
"recently_added_in": "Recientemente añadido en {{libraryName}}", "recently_added_in": "Recientemente añadido en {{libraryName}}",
"suggested_movies": "Películas sugeridas", "suggested_movies": "Películas sugeridas",
"suggested_episodes": "Episodios sugeridos",
"intro": { "intro": {
"welcome_to_streamyfin": "Bienvenido a Streamyfin", "welcome_to_streamyfin": "Bienvenido a Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Nada", "None": "Nada",
"OnlyForced": "Solo forzados" "OnlyForced": "Solo forzados"
}, },
"text_color": "Color del texto",
"background_color": "Color de fondo",
"outline_color": "Color de salida",
"outline_thickness": "Grosor exterior",
"background_opacity": "Opacidad de fondo",
"outline_opacity": "Opacidad exterior",
"bold_text": "Texto en negrita",
"colors": {
"Black": "Negro",
"Gray": "Gris",
"Silver": "Plata",
"White": "Blanco",
"Maroon": "Granate",
"Red": "Rojo",
"Fuchsia": "Fucsia",
"Yellow": "Amarillo",
"Olive": "Oliva",
"Green": "Verde",
"Teal": "Cereal",
"Lime": "Lima",
"Purple": "Morado",
"Navy": "Naval",
"Blue": "Azul",
"Aqua": "Agua"
},
"thickness": {
"None": "Ninguno",
"Thin": "Ligero",
"Normal": "Normal",
"Thick": "Grosor"
},
"subtitle_color": "Color de los Subtítulos",
"subtitle_background_color": "Color del fondo",
"subtitle_font": "Fuente de los subtítulos",
"ksplayer_title": "Ajustes de KSPlayer",
"hardware_decode": "Decodificación de hardware",
"hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "Configuración de subtítulos VLC",
"hint": "Personalizar la apariencia de los subtítulos para el reproductor VLC. Los cambios tendrán efecto en la próxima reproducción.",
"text_color": "Color del texto",
"background_color": "Color del fondo",
"background_opacity": "Opacidad del fondo",
"outline_color": "Color del contorno",
"outline_opacity": "Opacidad del contorno",
"outline_thickness": "Grosor del contorno",
"bold": "Texto en negrita",
"margin": "Margen inferior"
},
"video_player": {
"title": "Reproductor de vídeo",
"video_player": "Reproductor de vídeo",
"video_player_description": "Elige qué reproductor de vídeo en iOS",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Otros", "other_title": "Otros",
"video_orientation": "Orientación de vídeo", "video_orientation": "Orientación de vídeo",
@@ -351,13 +294,7 @@
"UNKNOWN": "Desconocida" "UNKNOWN": "Desconocida"
}, },
"safe_area_in_controls": "Área segura en controles", "safe_area_in_controls": "Área segura en controles",
"video_player": "Reproductor de vídeo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Mostrar enlaces de menú personalizados", "show_custom_menu_links": "Mostrar enlaces de menú personalizados",
"show_large_home_carousel": "Mostrar carrusel del menú principal grande (beta)",
"hide_libraries": "Ocultar bibliotecas", "hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
"disable_haptic_feedback": "Desactivar feedback háptico", "disable_haptic_feedback": "Desactivar feedback háptico",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Máximo número de episodios de Auto Play", "max_auto_play_episode_count": "Máximo número de episodios de Auto Play",
"disabled": "Deshabilitado" "disabled": "Deshabilitado"
}, },
"downloads": {
"downloads_title": "Descargas"
},
"music": { "music": {
"title": "Música", "title": "Música",
"playback_title": "Reproducir", "playback_title": "Reproducir",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Leer más sobre Marlin.", "read_more_about_marlin": "Leer más sobre Marlin.",
"save_button": "Guardar", "save_button": "Guardar",
"toasts": { "toasts": {
"saved": "Guardado", "saved": "Guardado"
"refreshed": "Ajustes del servidor actualizados" }
},
"refresh_from_server": "Actualizar ajustes del servidor"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Habilitar Streamystats",
"disable_streamystats": "Deshabilitar Streamystats", "disable_streamystats": "Deshabilitar Streamystats",
"enable_search": "Usar para la búsqueda", "enable_search": "Usar para la búsqueda",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.ejemplo.com", "server_url_placeholder": "http(s)://streamystats.ejemplo.com",
"streamystats_search_hint": "Introduzca la URL para su servidor Streamystats. La URL debe incluir http o https y opcionalmente el puerto.", "streamystats_search_hint": "Introduzca la URL para su servidor Streamystats. La URL debe incluir http o https y opcionalmente el puerto.",
"read_more_about_streamystats": "Leer más sobre Streamystats.", "read_more_about_streamystats": "Leer más sobre Streamystats.",
"save_button": "Guardar",
"save": "Guardar", "save": "Guardar",
"features_title": "Características", "features_title": "Características",
"home_sections_title": "Secciones de inicio",
"enable_movie_recommendations": "Recomendaciones de películas", "enable_movie_recommendations": "Recomendaciones de películas",
"enable_series_recommendations": "Recomendaciones de series", "enable_series_recommendations": "Recomendaciones de series",
"enable_promoted_watchlists": "Listas promocionadas", "enable_promoted_watchlists": "Listas promocionadas",
@@ -445,8 +374,7 @@
"refresh_from_server": "Actualizar ajustes desde el servidor" "refresh_from_server": "Actualizar ajustes desde el servidor"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Habilitar la integración de la lista de seguimiento", "watchlist_enabler": "Habilitar la integración de la lista de seguimiento"
"watchlist_button": "Activar o desactivar la integración de la lista de seguimiento"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Eliminar todos los archivos descargados", "delete_all_downloaded_files": "Eliminar todos los archivos descargados",
"music_cache_title": "Caché de música", "music_cache_title": "Caché de música",
"music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión", "music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión",
"enable_music_cache": "Activar Caché de Música",
"clear_music_cache": "Borrar Caché de Música", "clear_music_cache": "Borrar Caché de Música",
"music_cache_size": "Caché {{Tamaño}}", "music_cache_size": "Caché {{Tamaño}}",
"music_cache_cleared": "Caché de música eliminado", "music_cache_cleared": "Caché de música eliminado",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Nivel", "level": "Nivel",
"no_logs_available": "No hay registros disponibles", "no_logs_available": "No hay registros disponibles"
"delete_all_logs": "Eliminar todos los registros"
}, },
"languages": { "languages": {
"title": "Idiomas", "title": "Idiomas",
@@ -490,15 +414,12 @@
"system": "Sistema" "system": "Sistema"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error al eliminar archivos", "error_deleting_files": "Error al eliminar archivos"
"background_downloads_enabled": "Descargas en segundo plano habilitadas",
"background_downloads_disabled": "Descargas en segundo plano deshabilitadas"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Descargas", "downloads_title": "Descargas",
"tvseries": "Series", "tvseries": "Series",
"movies": "Películas", "movies": "Películas",
"queue": "Cola",
"other_media": "Otros medios", "other_media": "Otros medios",
"queue_hint": "La cola de series y películas se perderá al reiniciar la app",
"no_items_in_queue": "No hay ítems en la cola",
"no_downloaded_items": "No hay ítems descargados", "no_downloaded_items": "No hay ítems descargados",
"delete_all_movies_button": "Eliminar todas las películas", "delete_all_movies_button": "Eliminar todas las películas",
"delete_all_tvseries_button": "Eliminar todas las series", "delete_all_tvseries_button": "Eliminar todas las series",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Error al eliminar todas las series", "failed_to_delete_all_tvseries": "Error al eliminar todas las series",
"deleted_media_successfully": "¡Otros medios eliminados con éxito!", "deleted_media_successfully": "¡Otros medios eliminados con éxito!",
"failed_to_delete_media": "Error al eliminar otros medios", "failed_to_delete_media": "Error al eliminar otros medios",
"download_deleted": "Descarga eliminada",
"download_cancelled": "Descarga cancelada", "download_cancelled": "Descarga cancelada",
"could_not_delete_download": "No se pudo eliminar la descarga", "could_not_delete_download": "No se pudo eliminar la descarga",
"download_paused": "Descarga pausada",
"could_not_pause_download": "No se pudo pausar la descarga",
"download_resumed": "Descarga rebatida",
"could_not_resume_download": "No se pudo reiniciar la descarga",
"download_completed": "Descarga completada", "download_completed": "Descarga completada",
"download_failed": "Descarga fallida", "download_failed": "Descarga fallida",
"download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} ya está descargando", "item_already_downloading": "{{item}} ya está descargando",
"all_files_deleted": "Todas las descargas eliminadas correctamente", "all_files_deleted": "Todas las descargas eliminadas correctamente",
"files_deleted_by_type": "{{count}} {{type}} eliminado", "files_deleted_by_type": "{{count}} {{type}} eliminado",
"all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito",
"failed_to_clean_cache_directory": "Error al limpiar el directorio de caché",
"could_not_get_download_url_for_item": "No se pudo obtener la URL de descarga para {{itemName}}", "could_not_get_download_url_for_item": "No se pudo obtener la URL de descarga para {{itemName}}",
"go_to_downloads": "Ir a descargas",
"file_deleted": "{{item}} eliminado" "file_deleted": "{{item}} eliminado"
} }
} }
@@ -583,16 +493,13 @@
"none": "Nada", "none": "Nada",
"track": "Pista", "track": "Pista",
"cancel": "Cancelar", "cancel": "Cancelar",
"stop": "Stop",
"delete": "Borrar", "delete": "Borrar",
"ok": "Aceptar", "ok": "Aceptar",
"remove": "Eliminar", "remove": "Eliminar",
"next": "Siguiente",
"back": "Atrás", "back": "Atrás",
"continue": "Continuar", "continue": "Continuar",
"verifying": "Verificando...", "verifying": "Verificando...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Buscar...", "search": "Buscar...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "No se pudo crear el Steam para Chromecast", "could_not_create_stream_for_chromecast": "No se pudo crear el Steam para Chromecast",
"message_from_server": "Mensaje del servidor: {{message}}", "message_from_server": "Mensaje del servidor: {{message}}",
"next_episode": "Siguiente episodio", "next_episode": "Siguiente episodio",
"refresh_tracks": "Refrescar pistas",
"audio_tracks": "Pistas de audio:",
"playback_state": "Estado de la reproducción:",
"index": "Índice:",
"continue_watching": "Continuar viendo", "continue_watching": "Continuar viendo",
"go_back": "Volver", "go_back": "Volver",
"downloaded_file_title": "Ya tienes este archivo descargado", "downloaded_file_title": "Ya tienes este archivo descargado",
@@ -761,7 +664,6 @@
"show_more": "Mostrar más", "show_more": "Mostrar más",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Listas de reproducción", "playlists": "Listas de reproducción",
"tracks": "Canciones" "tracks": "Canciones"
}, },
"filters": {
"all": "Todas"
},
"recently_added": "Recientemente añadido", "recently_added": "Recientemente añadido",
"recently_played": "Reproducidos Recientemente", "recently_played": "Reproducidos Recientemente",
"frequently_played": "Reproducido con frecuencia", "frequently_played": "Reproducido con frecuencia",
"explore": "Explorar",
"top_tracks": "Canciones Populares", "top_tracks": "Canciones Populares",
"play": "Reproducir", "play": "Reproducir",
"shuffle": "Aleatorio", "shuffle": "Aleatorio",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon", "recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon",
"suggested_movies": "Ehdotetut elokuvat", "suggested_movies": "Ehdotetut elokuvat",
"suggested_episodes": "Ehdotetut jaksot",
"intro": { "intro": {
"welcome_to_streamyfin": "Tervetuloa Streamyfiniin", "welcome_to_streamyfin": "Tervetuloa Streamyfiniin",
"a_free_and_open_source_client_for_jellyfin": "Ilmainen ja avoimen lähdekoodin asiakas Jellyfinille.", "a_free_and_open_source_client_for_jellyfin": "Ilmainen ja avoimen lähdekoodin asiakas Jellyfinille.",
@@ -261,43 +260,6 @@
"None": "Ei mitään", "None": "Ei mitään",
"OnlyForced": "Vain pakotettu" "OnlyForced": "Vain pakotettu"
}, },
"text_color": "Tekstin väri",
"background_color": "Taustaväri",
"outline_color": "Ääriviivan väri",
"outline_thickness": "Ääriviivan paksuus",
"background_opacity": "Taustan läpinäkyvyys",
"outline_opacity": "Ääriviivan Läpinäkyvyys",
"bold_text": "Lihavoi teksti",
"colors": {
"Black": "Musta",
"Gray": "Harmaa",
"Silver": "Hopea",
"White": "Valkoinen",
"Maroon": "Maroon",
"Red": "Punainen",
"Fuchsia": "Fuchsia",
"Yellow": "Keltainen",
"Olive": "Oliivit",
"Green": "Vihreä",
"Teal": "Sinappi",
"Lime": "Limea",
"Purple": "Violetti",
"Navy": "Laiva",
"Blue": "Sininen",
"Aqua": "Vesi"
},
"thickness": {
"None": "Ei mitään",
"Thin": "Ohut",
"Normal": "Normaali",
"Thick": "Paksu"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Muut", "other_title": "Muut",
"video_orientation": "Videon suunta", "video_orientation": "Videon suunta",
@@ -351,13 +294,7 @@
"UNKNOWN": "Tuntematon" "UNKNOWN": "Tuntematon"
}, },
"safe_area_in_controls": "Turvallinen alue ohjaimissa", "safe_area_in_controls": "Turvallinen alue ohjaimissa",
"video_player": "Videosoitin",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Kokeellinen + PiP)"
},
"show_custom_menu_links": "Näytä mukautetut valikkolinkit", "show_custom_menu_links": "Näytä mukautetut valikkolinkit",
"show_large_home_carousel": "Näytä suuri kotikaruselli (beta)",
"hide_libraries": "Piilota kirjastot", "hide_libraries": "Piilota kirjastot",
"select_liraries_you_want_to_hide": "Valitse kirjastot, jotka haluat piilottaa Kirjasto-välilehdeltä ja etusivun osioista.", "select_liraries_you_want_to_hide": "Valitse kirjastot, jotka haluat piilottaa Kirjasto-välilehdeltä ja etusivun osioista.",
"disable_haptic_feedback": "Poista haptinen palautteet käytöstä", "disable_haptic_feedback": "Poista haptinen palautteet käytöstä",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Automaattisten Toistojaksojen Maksimimäärä", "max_auto_play_episode_count": "Automaattisten Toistojaksojen Maksimimäärä",
"disabled": "Pois Käytöstä" "disabled": "Pois Käytöstä"
}, },
"downloads": {
"downloads_title": "Lataukset"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Lue lisää Marlinista.", "read_more_about_marlin": "Lue lisää Marlinista.",
"save_button": "Tallenna", "save_button": "Tallenna",
"toasts": { "toasts": {
"saved": "Tallennettu", "saved": "Tallennettu"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Poista kaikki ladatut tiedostot", "delete_all_downloaded_files": "Poista kaikki ladatut tiedostot",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Vie lokit", "export_logs": "Vie lokit",
"click_for_more_info": "Napsauta lisätietoja varten", "click_for_more_info": "Napsauta lisätietoja varten",
"level": "Taso", "level": "Taso",
"no_logs_available": "Ei lokitietoja saatavilla", "no_logs_available": "Ei lokitietoja saatavilla"
"delete_all_logs": "Poista kaikki lokit"
}, },
"languages": { "languages": {
"title": "Kielet", "title": "Kielet",
@@ -490,15 +414,12 @@
"system": "Järjestelmä" "system": "Järjestelmä"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Virhe tiedostojen poistamisessa", "error_deleting_files": "Virhe tiedostojen poistamisessa"
"background_downloads_enabled": "Taustalataukset käytössä",
"background_downloads_disabled": "Taustalataukset pois käytöstä"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Lataukset", "downloads_title": "Lataukset",
"tvseries": "TV-sarjat", "tvseries": "TV-sarjat",
"movies": "Elokuvat", "movies": "Elokuvat",
"queue": "Jonot",
"other_media": "Muu media", "other_media": "Muu media",
"queue_hint": "Jonot ja lataukset menetetään sovelluksen uudelleenkäynnistyksen yhteydessä",
"no_items_in_queue": "Ei kohteita jonossa",
"no_downloaded_items": "Ei ladattuja kohteita", "no_downloaded_items": "Ei ladattuja kohteita",
"delete_all_movies_button": "Poista kaikki elokuvat", "delete_all_movies_button": "Poista kaikki elokuvat",
"delete_all_tvseries_button": "Poista kaikki TV-sarjat", "delete_all_tvseries_button": "Poista kaikki TV-sarjat",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Kaikkien TV-sarjojen poistaminen epäonnistui", "failed_to_delete_all_tvseries": "Kaikkien TV-sarjojen poistaminen epäonnistui",
"deleted_media_successfully": "Muu media poistettu onnistuneesti!", "deleted_media_successfully": "Muu media poistettu onnistuneesti!",
"failed_to_delete_media": "Muiden medioiden poistaminen epäonnistui", "failed_to_delete_media": "Muiden medioiden poistaminen epäonnistui",
"download_deleted": "Lataus Poistettu",
"download_cancelled": "Lataus peruutettu", "download_cancelled": "Lataus peruutettu",
"could_not_delete_download": "Latausta Ei Voitu Poistaa", "could_not_delete_download": "Latausta Ei Voitu Poistaa",
"download_paused": "Lataus Keskeytetty",
"could_not_pause_download": "Latausta Ei Voitu Keskeyttää",
"download_resumed": "Lataus Jatketaan",
"could_not_resume_download": "Latausta Ei Voitu Jatkaa.",
"download_completed": "Lataus valmis", "download_completed": "Lataus valmis",
"download_failed": "Lataus epäonnistui", "download_failed": "Lataus epäonnistui",
"download_failed_for_item": "Lataus epäonnistui kohteelle {{item}} - {{error}}", "download_failed_for_item": "Lataus epäonnistui kohteelle {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "Kaikki lataukset poistettu onnistuneesti", "all_files_deleted": "Kaikki lataukset poistettu onnistuneesti",
"files_deleted_by_type": "{{count}} {{type}} poistettu", "files_deleted_by_type": "{{count}} {{type}} poistettu",
"all_files_folders_and_jobs_deleted_successfully": "Kaikki tiedostot, kansiot ja tehtävät poistettu onnistuneesti",
"failed_to_clean_cache_directory": "Välimuistin hakemiston puhdistus epäonnistui",
"could_not_get_download_url_for_item": "Latauksen URL-osoitetta ei voitu ladata {{itemName}}", "could_not_get_download_url_for_item": "Latauksen URL-osoitetta ei voitu ladata {{itemName}}",
"go_to_downloads": "Siirry latauksiin",
"file_deleted": "{{item}} poistettu" "file_deleted": "{{item}} poistettu"
} }
} }
@@ -583,16 +493,13 @@
"none": "Ei mitään", "none": "Ei mitään",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Haku...", "search": "Haku...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Suoratoistoa ei voitu luoda Chromecastia varten", "could_not_create_stream_for_chromecast": "Suoratoistoa ei voitu luoda Chromecastia varten",
"message_from_server": "Viesti palvelimelta: {{message}}", "message_from_server": "Viesti palvelimelta: {{message}}",
"next_episode": "Seuraava Jakso", "next_episode": "Seuraava Jakso",
"refresh_tracks": "Päivitä Kappaleet",
"audio_tracks": "Ääni Kappaleet:",
"playback_state": "Toiston Tila:",
"index": "Indeksi:",
"continue_watching": "Jatka katsomista", "continue_watching": "Jatka katsomista",
"go_back": "Siirry Takaisin", "go_back": "Siirry Takaisin",
"downloaded_file_title": "Tämä tiedosto on ladattuna", "downloaded_file_title": "Tämä tiedosto on ladattuna",
@@ -761,7 +664,6 @@
"show_more": "Näytä Lisää", "show_more": "Näytä Lisää",
"show_less": "Näytä Vähemmän", "show_less": "Näytä Vähemmän",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

File diff suppressed because it is too large Load Diff

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "התווסף לאחרונה ב-{{libraryName}}", "recently_added_in": "התווסף לאחרונה ב-{{libraryName}}",
"suggested_movies": "סרטים מוצעים", "suggested_movies": "סרטים מוצעים",
"suggested_episodes": "פרקים מוצעים",
"intro": { "intro": {
"welcome_to_streamyfin": "ברוך הבא ל-Streamyfin", "welcome_to_streamyfin": "ברוך הבא ל-Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "קליינט חינמי ובקוד פתוח לשרתי Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "קליינט חינמי ובקוד פתוח לשרתי Jellyfin.",
@@ -261,43 +260,6 @@
"None": "ללא", "None": "ללא",
"OnlyForced": "רק כפוי" "OnlyForced": "רק כפוי"
}, },
"text_color": "צבע הטקסט",
"background_color": "צבע רקע",
"outline_color": "צבע קו מתאר",
"outline_thickness": "עובי קו מתאר",
"background_opacity": "שקיפות רקע",
"outline_opacity": "אטימות קו מתאר",
"bold_text": "טקסט בולט",
"colors": {
"Black": "שחור",
"Gray": "אפור",
"Silver": "כסף",
"White": "לבן",
"Maroon": "חום ערמוני",
"Red": "אדום",
"Fuchsia": "פוקסיה",
"Yellow": "צהוב",
"Olive": "זית",
"Green": "ירוק",
"Teal": "תכלת",
"Lime": "ירוק ליים",
"Purple": "סגול",
"Navy": "כחול כהה",
"Blue": "כחול",
"Aqua": "כחול בהיר"
},
"thickness": {
"None": "ללא",
"Thin": "דק",
"Normal": "רגיל",
"Thick": "עבה"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "נגן וידאו",
"video_player": "נגן וידאו",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "אחר", "other_title": "אחר",
"video_orientation": "כיוון וידיאו", "video_orientation": "כיוון וידיאו",
@@ -351,13 +294,7 @@
"UNKNOWN": "לא ידוע" "UNKNOWN": "לא ידוע"
}, },
"safe_area_in_controls": "איזור בטוח בפקדים", "safe_area_in_controls": "איזור בטוח בפקדים",
"video_player": "נגן וידאו",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (ניסיוני + נגן בתוך נגן)"
},
"show_custom_menu_links": "הצג קישורים לתפריטים מותאמים אישית", "show_custom_menu_links": "הצג קישורים לתפריטים מותאמים אישית",
"show_large_home_carousel": "הצג קרוסלה גדולה במסך הבית (בטא)",
"hide_libraries": "הסתר ספריות", "hide_libraries": "הסתר ספריות",
"select_liraries_you_want_to_hide": "בחר את הספריות שתרצה להסתיר ממסך הספריות וגם ממסך הבית.", "select_liraries_you_want_to_hide": "בחר את הספריות שתרצה להסתיר ממסך הספריות וגם ממסך הבית.",
"disable_haptic_feedback": "בטל משוב רטט", "disable_haptic_feedback": "בטל משוב רטט",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "כמות פרקים מקסימלית לניגון אוטומטי", "max_auto_play_episode_count": "כמות פרקים מקסימלית לניגון אוטומטי",
"disabled": "כבוי" "disabled": "כבוי"
}, },
"downloads": {
"downloads_title": "הורדות"
},
"music": { "music": {
"title": "מוזיקה", "title": "מוזיקה",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "קרא עוד על Marlin.", "read_more_about_marlin": "קרא עוד על Marlin.",
"save_button": "שמור", "save_button": "שמור",
"toasts": { "toasts": {
"saved": "נשמר", "saved": "נשמר"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "מחק את כל הקבצים שהורדו", "delete_all_downloaded_files": "מחק את כל הקבצים שהורדו",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "ייצוא לוגים", "export_logs": "ייצוא לוגים",
"click_for_more_info": "לחץ למידע נוסף", "click_for_more_info": "לחץ למידע נוסף",
"level": "רמה", "level": "רמה",
"no_logs_available": "אין לוגים זמינים", "no_logs_available": "אין לוגים זמינים"
"delete_all_logs": "מחק את כל הלוגים"
}, },
"languages": { "languages": {
"title": "שפות", "title": "שפות",
@@ -490,15 +414,12 @@
"system": "מערכת" "system": "מערכת"
}, },
"toasts": { "toasts": {
"error_deleting_files": "שגיאה במחיקת קבצים", "error_deleting_files": "שגיאה במחיקת קבצים"
"background_downloads_enabled": "הורדה ברקע מופעלת",
"background_downloads_disabled": "הורדה ברקע כבויה"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "הורדות", "downloads_title": "הורדות",
"tvseries": "סדרות", "tvseries": "סדרות",
"movies": "סרטים", "movies": "סרטים",
"queue": "תוֹר",
"other_media": "תוכן אחר", "other_media": "תוכן אחר",
"queue_hint": "התור וההורדות יאבדו בפתיחה מחדש של האפליקציה",
"no_items_in_queue": "אין פרטים בתור",
"no_downloaded_items": "אין פריטים שהורדו", "no_downloaded_items": "אין פריטים שהורדו",
"delete_all_movies_button": "מחק את כל הסרטים", "delete_all_movies_button": "מחק את כל הסרטים",
"delete_all_tvseries_button": "מחק את כל הסדרות", "delete_all_tvseries_button": "מחק את כל הסדרות",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "נכשל במחיקת כל הסדרות", "failed_to_delete_all_tvseries": "נכשל במחיקת כל הסדרות",
"deleted_media_successfully": "כל שאר התוכן נמחק בהצלחה!", "deleted_media_successfully": "כל שאר התוכן נמחק בהצלחה!",
"failed_to_delete_media": "נכשל במחיקת שאר התוכן", "failed_to_delete_media": "נכשל במחיקת שאר התוכן",
"download_deleted": "ההורדה נמחקה",
"download_cancelled": "ההורדה בוטלה", "download_cancelled": "ההורדה בוטלה",
"could_not_delete_download": "לא היה ניתן למחוק את ההורדה", "could_not_delete_download": "לא היה ניתן למחוק את ההורדה",
"download_paused": "ההורדה נעצרה",
"could_not_pause_download": "לא היה ניתן לעצור את ההורדה",
"download_resumed": "ההורדה חודשה",
"could_not_resume_download": "לא היה ניתן לחדש את ההורדה",
"download_completed": "ההורדה הושלמה", "download_completed": "ההורדה הושלמה",
"download_failed": "ההורדה נכשלה", "download_failed": "ההורדה נכשלה",
"download_failed_for_item": "ההורדה נכשלה עבור {{item}} - {{error}}", "download_failed_for_item": "ההורדה נכשלה עבור {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} כבר נמצא בהורדה", "item_already_downloading": "{{item}} כבר נמצא בהורדה",
"all_files_deleted": "כל ההורדות נמחקו בהצלחה", "all_files_deleted": "כל ההורדות נמחקו בהצלחה",
"files_deleted_by_type": "{{count}} {{type}} נמחקו", "files_deleted_by_type": "{{count}} {{type}} נמחקו",
"all_files_folders_and_jobs_deleted_successfully": "כל הקבצים, התיקיות והעבודות נמחקו בהצלחה",
"failed_to_clean_cache_directory": "נכשל בניסיון למחוק את תיקיית המטמון",
"could_not_get_download_url_for_item": "לא היה ניתן להשיג את קישור ההורדה של {{itemName}}", "could_not_get_download_url_for_item": "לא היה ניתן להשיג את קישור ההורדה של {{itemName}}",
"go_to_downloads": "עבור להורדות",
"file_deleted": "{{item}} נמחק" "file_deleted": "{{item}} נמחק"
} }
} }
@@ -583,16 +493,13 @@
"none": "ללא", "none": "ללא",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "חפש...", "search": "חפש...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "נכשל ביצירת זרם עבור Chromecast", "could_not_create_stream_for_chromecast": "נכשל ביצירת זרם עבור Chromecast",
"message_from_server": "הודעה מהשרת: {{message}}", "message_from_server": "הודעה מהשרת: {{message}}",
"next_episode": "הפרק הבא", "next_episode": "הפרק הבא",
"refresh_tracks": "רענן רצועות",
"audio_tracks": "רצועות שמע:",
"playback_state": "מצב ניגון:",
"index": "מיקום:",
"continue_watching": "המשך לצפות", "continue_watching": "המשך לצפות",
"go_back": "חזור", "go_back": "חזור",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "הצג עוד", "show_more": "הצג עוד",
"show_less": "הצג פחות", "show_less": "הצג פחות",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Új a(z) {{libraryName}} könyvtárban", "recently_added_in": "Új a(z) {{libraryName}} könyvtárban",
"suggested_movies": "Javasolt Filmek", "suggested_movies": "Javasolt Filmek",
"suggested_episodes": "Javasolt Epizódok",
"intro": { "intro": {
"welcome_to_streamyfin": "Üdvözöljük a Streamyfinben", "welcome_to_streamyfin": "Üdvözöljük a Streamyfinben",
"a_free_and_open_source_client_for_jellyfin": "Egy Ingyenes és Nyílt Forráskódú Jellyfin Kliens.", "a_free_and_open_source_client_for_jellyfin": "Egy Ingyenes és Nyílt Forráskódú Jellyfin Kliens.",
@@ -261,43 +260,6 @@
"None": "Nincs", "None": "Nincs",
"OnlyForced": "Csak Kényszerített" "OnlyForced": "Csak Kényszerített"
}, },
"text_color": "Szövegszín",
"background_color": "Háttérszín",
"outline_color": "Körvonal színe",
"outline_thickness": "Körvonal Vastagsága",
"background_opacity": "Háttér Áttetszőség",
"outline_opacity": "Körvonal Áttetszőség",
"bold_text": "Félkövér Szöveg",
"colors": {
"Black": "Fekete",
"Gray": "Szürke",
"Silver": "Ezüst",
"White": "Fehér",
"Maroon": "Sötétvörös",
"Red": "Piros",
"Fuchsia": "Fukszia",
"Yellow": "Sárga",
"Olive": "Oliva",
"Green": "Zöld",
"Teal": "Türkiz",
"Lime": "Lime",
"Purple": "Lila",
"Navy": "Sötétkék",
"Blue": "Kék",
"Aqua": "Türkizkék"
},
"thickness": {
"None": "Nincs",
"Thin": "Vékony",
"Normal": "Normál",
"Thick": "Vastag"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Egyéb", "other_title": "Egyéb",
"video_orientation": "Videó Tájolás", "video_orientation": "Videó Tájolás",
@@ -351,13 +294,7 @@
"UNKNOWN": "Ismeretlen" "UNKNOWN": "Ismeretlen"
}, },
"safe_area_in_controls": "Biztonsági Sáv a Vezérlőkben", "safe_area_in_controls": "Biztonsági Sáv a Vezérlőkben",
"video_player": "Videólejátszó",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Kísérleti + PiP)"
},
"show_custom_menu_links": "Egyéni Menülinkek Megjelenítése", "show_custom_menu_links": "Egyéni Menülinkek Megjelenítése",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Könyvtárak Elrejtése", "hide_libraries": "Könyvtárak Elrejtése",
"select_liraries_you_want_to_hide": "Válaszd ki azokat a könyvtárakat, amelyeket el szeretnél rejteni a Könyvtár fülön és a kezdőlapon.", "select_liraries_you_want_to_hide": "Válaszd ki azokat a könyvtárakat, amelyeket el szeretnél rejteni a Könyvtár fülön és a kezdőlapon.",
"disable_haptic_feedback": "Haptikus Visszajelzés Letiltása", "disable_haptic_feedback": "Haptikus Visszajelzés Letiltása",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max. Auto. Epizódlejátszás", "max_auto_play_episode_count": "Max. Auto. Epizódlejátszás",
"disabled": "Letiltva" "disabled": "Letiltva"
}, },
"downloads": {
"downloads_title": "Letöltések"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Tudj Meg Többet a Marlinról", "read_more_about_marlin": "Tudj Meg Többet a Marlinról",
"save_button": "Mentés", "save_button": "Mentés",
"toasts": { "toasts": {
"saved": "Mentve", "saved": "Mentve"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Minden Letöltött Fájl Törlése", "delete_all_downloaded_files": "Minden Letöltött Fájl Törlése",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Naplók Exportálása", "export_logs": "Naplók Exportálása",
"click_for_more_info": "Kattints a Részletekért", "click_for_more_info": "Kattints a Részletekért",
"level": "Szint", "level": "Szint",
"no_logs_available": "Nincsenek Naplók", "no_logs_available": "Nincsenek Naplók"
"delete_all_logs": "Összes Napló Törlése"
}, },
"languages": { "languages": {
"title": "Nyelvek", "title": "Nyelvek",
@@ -490,15 +414,12 @@
"system": "Rendszer" "system": "Rendszer"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Hiba a Fájlok Törlésekor", "error_deleting_files": "Hiba a Fájlok Törlésekor"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Letöltések", "downloads_title": "Letöltések",
"tvseries": "Sorozatok", "tvseries": "Sorozatok",
"movies": "Filmek", "movies": "Filmek",
"queue": "Sor",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "A sor és a letöltések az alkalmazás újraindításakor elvesznek",
"no_items_in_queue": "Nincs Elem a Sorban",
"no_downloaded_items": "Nincsenek Letöltött Elemek", "no_downloaded_items": "Nincsenek Letöltött Elemek",
"delete_all_movies_button": "Összes Film Törlése", "delete_all_movies_button": "Összes Film Törlése",
"delete_all_tvseries_button": "Összes Sorozat Törlése", "delete_all_tvseries_button": "Összes Sorozat Törlése",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Nem Sikerült Törölni Az Összes Sorozatot", "failed_to_delete_all_tvseries": "Nem Sikerült Törölni Az Összes Sorozatot",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Letöltés Törölve",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Nem Sikerült Törölni a Letöltést", "could_not_delete_download": "Nem Sikerült Törölni a Letöltést",
"download_paused": "Letöltés Szüneteltetve",
"could_not_pause_download": "Nem Sikerült Szüneteltetni a Letöltést",
"download_resumed": "Letöltés Folytatva",
"could_not_resume_download": "Nem Sikerült Folytatni a Letöltést",
"download_completed": "Letöltés Befejezve", "download_completed": "Letöltés Befejezve",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "A(z) {{item}} letöltése sikertelen - {{error}}", "download_failed_for_item": "A(z) {{item}} letöltése sikertelen - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Minden fájl, mappa és feladat sikeresen törölve",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Ugrás a Letöltésekhez",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Keresés...", "search": "Keresés...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "A Chromecast stream létrehozása sikertelen volt", "could_not_create_stream_for_chromecast": "A Chromecast stream létrehozása sikertelen volt",
"message_from_server": "Üzenet a szervertől: {{message}}", "message_from_server": "Üzenet a szervertől: {{message}}",
"next_episode": "Következő Epizód", "next_episode": "Következő Epizód",
"refresh_tracks": "Sávok Frissítése",
"audio_tracks": "Hangsávok:",
"playback_state": "Lejátszás Állapota:",
"index": "Index:",
"continue_watching": "Folytatás", "continue_watching": "Folytatás",
"go_back": "Vissza", "go_back": "Vissza",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Több Megjelenítése", "show_more": "Több Megjelenítése",
"show_less": "Kevesebb Megjelenítése", "show_less": "Kevesebb Megjelenítése",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Aggiunti di recente a {{libraryName}}", "recently_added_in": "Aggiunti di recente a {{libraryName}}",
"suggested_movies": "Film consigliati", "suggested_movies": "Film consigliati",
"suggested_episodes": "Episodi consigliati",
"intro": { "intro": {
"welcome_to_streamyfin": "Benvenuto a Streamyfin", "welcome_to_streamyfin": "Benvenuto a Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Nessuno", "None": "Nessuno",
"OnlyForced": "Solo forzati" "OnlyForced": "Solo forzati"
}, },
"text_color": "Colore Del Testo",
"background_color": "Colore Di Sfondo",
"outline_color": "Colore Contorno",
"outline_thickness": "Spessore Contorno",
"background_opacity": "Opacità Dello Sfondo",
"outline_opacity": "Opacità Contorno",
"bold_text": "Bold Text",
"colors": {
"Black": "Nero",
"Gray": "Grigio",
"Silver": "Argento",
"White": "Bianco",
"Maroon": "Maroon",
"Red": "Rosso",
"Fuchsia": "Fuchsia",
"Yellow": "Giallo",
"Olive": "Olive",
"Green": "Verde",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Viola",
"Navy": "Marina",
"Blue": "Blu",
"Aqua": "Aqua"
},
"thickness": {
"None": "Nessuno",
"Thin": "Sottile",
"Normal": "Normale",
"Thick": "Spessa"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Altro", "other_title": "Altro",
"video_orientation": "Orientamento del video", "video_orientation": "Orientamento del video",
@@ -351,13 +294,7 @@
"UNKNOWN": "Sconosciuto" "UNKNOWN": "Sconosciuto"
}, },
"safe_area_in_controls": "Area sicura per i controlli", "safe_area_in_controls": "Area sicura per i controlli",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Sperimentale + PiP)"
},
"show_custom_menu_links": "Mostra i link del menu personalizzato", "show_custom_menu_links": "Mostra i link del menu personalizzato",
"show_large_home_carousel": "Mostra Carosello Grande nella Home (beta)",
"hide_libraries": "Nascondi Librerie", "hide_libraries": "Nascondi Librerie",
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
"disable_haptic_feedback": "Disabilita il feedback aptico", "disable_haptic_feedback": "Disabilita il feedback aptico",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Numero Massimo Di Episodi Riproduzione Automatica", "max_auto_play_episode_count": "Numero Massimo Di Episodi Riproduzione Automatica",
"disabled": "Disabilitato" "disabled": "Disabilitato"
}, },
"downloads": {
"downloads_title": "Scaricamento"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Leggi di più su Marlin.", "read_more_about_marlin": "Leggi di più su Marlin.",
"save_button": "Salva", "save_button": "Salva",
"toasts": { "toasts": {
"saved": "Salvato", "saved": "Salvato"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"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": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "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"
}, },
"languages": { "languages": {
"title": "Lingue", "title": "Lingue",
@@ -490,15 +414,12 @@
"system": "Sistema" "system": "Sistema"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Errore nella cancellazione dei file", "error_deleting_files": "Errore nella cancellazione dei file"
"background_downloads_enabled": "Scaricamento in background abilitato",
"background_downloads_disabled": "Scaricamento in background disabilitato"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Scaricati", "downloads_title": "Scaricati",
"tvseries": "Serie TV", "tvseries": "Serie TV",
"movies": "Film", "movies": "Film",
"queue": "Coda",
"other_media": "Altri supporti", "other_media": "Altri supporti",
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
"no_items_in_queue": "Nessun elemento in coda",
"no_downloaded_items": "Nessun elemento scaricato", "no_downloaded_items": "Nessun elemento scaricato",
"delete_all_movies_button": "Cancella tutti i film", "delete_all_movies_button": "Cancella tutti i film",
"delete_all_tvseries_button": "Cancella tutte le serie TV", "delete_all_tvseries_button": "Cancella tutte le serie TV",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
"deleted_media_successfully": "Eliminato altri supporti con successo!", "deleted_media_successfully": "Eliminato altri supporti con successo!",
"failed_to_delete_media": "Impossibile eliminare altri media", "failed_to_delete_media": "Impossibile eliminare altri media",
"download_deleted": "Download Eliminato",
"download_cancelled": "Scaricamento annullato", "download_cancelled": "Scaricamento annullato",
"could_not_delete_download": "Impossibile Eliminare Il Download", "could_not_delete_download": "Impossibile Eliminare Il Download",
"download_paused": "Download In Pausa",
"could_not_pause_download": "Impossibile Sbloccare Il Download",
"download_resumed": "Download Ripreso",
"could_not_resume_download": "Impossibile Riprendere Il Download",
"download_completed": "Scaricamento completato", "download_completed": "Scaricamento completato",
"download_failed": "Scaricamento non riuscito", "download_failed": "Scaricamento non riuscito",
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} è già in download", "item_already_downloading": "{{item}} è già in download",
"all_files_deleted": "Tutti i Download Eliminati con Successo", "all_files_deleted": "Tutti i Download Eliminati con Successo",
"files_deleted_by_type": "{{count}} {{type}} cancellati", "files_deleted_by_type": "{{count}} {{type}} cancellati",
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
"failed_to_clean_cache_directory": "Pulizia della directory della cache non riuscita",
"could_not_get_download_url_for_item": "Impossibile ottenere l'URL di download per {{itemName}}", "could_not_get_download_url_for_item": "Impossibile ottenere l'URL di download per {{itemName}}",
"go_to_downloads": "Vai agli elementi scaricati",
"file_deleted": "{{item}} cancellato" "file_deleted": "{{item}} cancellato"
} }
} }
@@ -583,16 +493,13 @@
"none": "Nulla", "none": "Nulla",
"track": "Traccia", "track": "Traccia",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
"message_from_server": "Messaggio dal server", "message_from_server": "Messaggio dal server",
"next_episode": "Prossimo Episodio", "next_episode": "Prossimo Episodio",
"refresh_tracks": "Aggiorna tracce",
"audio_tracks": "Tracce audio:",
"playback_state": "Stato della riproduzione:",
"index": "Indice:",
"continue_watching": "Continua a guardare", "continue_watching": "Continua a guardare",
"go_back": "Indietro", "go_back": "Indietro",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Mostra di più", "show_more": "Mostra di più",
"show_less": "Mostra di meno", "show_less": "Mostra di meno",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "{{libraryName}}に最近追加された", "recently_added_in": "{{libraryName}}に最近追加された",
"suggested_movies": "おすすめ映画", "suggested_movies": "おすすめ映画",
"suggested_episodes": "おすすめエピソード",
"intro": { "intro": {
"welcome_to_streamyfin": "Streamyfinへようこそ", "welcome_to_streamyfin": "Streamyfinへようこそ",
"a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。",
@@ -261,43 +260,6 @@
"None": "なし", "None": "なし",
"OnlyForced": "強制のみ" "OnlyForced": "強制のみ"
}, },
"text_color": "テキストの色",
"background_color": "背景色",
"outline_color": "アウトラインの色",
"outline_thickness": "概要 厚さ",
"background_opacity": "背景の透明度",
"outline_opacity": "アウトラインの透明度",
"bold_text": "Bold Text",
"colors": {
"Black": "ブラック",
"Gray": "グレー",
"Silver": "シルバー",
"White": "白",
"Maroon": "Maroon",
"Red": "赤",
"Fuchsia": "Fuchsia",
"Yellow": "黄色",
"Olive": "オリーブ",
"Green": "緑",
"Teal": "ティール",
"Lime": "黄緑",
"Purple": "パープル",
"Navy": "海軍format@@0",
"Blue": "青",
"Aqua": "Aqua"
},
"thickness": {
"None": "なし",
"Thin": "細いです",
"Normal": "標準",
"Thick": "濃厚な"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "その他", "other_title": "その他",
"video_orientation": "動画の向き", "video_orientation": "動画の向き",
@@ -351,13 +294,7 @@
"UNKNOWN": "不明" "UNKNOWN": "不明"
}, },
"safe_area_in_controls": "コントロールの安全エリア", "safe_area_in_controls": "コントロールの安全エリア",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "カスタムメニューのリンクを表示", "show_custom_menu_links": "カスタムメニューのリンクを表示",
"show_large_home_carousel": "大きなヒーローBeta",
"hide_libraries": "ライブラリを非表示", "hide_libraries": "ライブラリを非表示",
"select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
"disable_haptic_feedback": "触覚フィードバックを無効にする", "disable_haptic_feedback": "触覚フィードバックを無効にする",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "自動再生エピソードの最大数", "max_auto_play_episode_count": "自動再生エピソードの最大数",
"disabled": "無効" "disabled": "無効"
}, },
"downloads": {
"downloads_title": "ダウンロード"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Marlinについて詳しく読む。", "read_more_about_marlin": "Marlinについて詳しく読む。",
"save_button": "保存", "save_button": "保存",
"toasts": { "toasts": {
"saved": "保存しました", "saved": "保存しました"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "すべてのダウンロードファイルを削除", "delete_all_downloaded_files": "すべてのダウンロードファイルを削除",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "レベル", "level": "レベル",
"no_logs_available": "ログがありません", "no_logs_available": "ログがありません"
"delete_all_logs": "すべてのログを削除"
}, },
"languages": { "languages": {
"title": "言語", "title": "言語",
@@ -490,15 +414,12 @@
"system": "システム" "system": "システム"
}, },
"toasts": { "toasts": {
"error_deleting_files": "ファイルの削除エラー", "error_deleting_files": "ファイルの削除エラー"
"background_downloads_enabled": "バックグラウンドでのダウンロードは有効です",
"background_downloads_disabled": "バックグラウンドでのダウンロードは無効です"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "ダウンロード", "downloads_title": "ダウンロード",
"tvseries": "TVシリーズ", "tvseries": "TVシリーズ",
"movies": "映画", "movies": "映画",
"queue": "キュー",
"other_media": "その他のメディア", "other_media": "その他のメディア",
"queue_hint": "アプリを再起動するとキューとダウンロードは失われます",
"no_items_in_queue": "キューにアイテムがありません",
"no_downloaded_items": "ダウンロードしたアイテムはありません", "no_downloaded_items": "ダウンロードしたアイテムはありません",
"delete_all_movies_button": "すべての映画を削除", "delete_all_movies_button": "すべての映画を削除",
"delete_all_tvseries_button": "すべてのシリーズを削除", "delete_all_tvseries_button": "すべてのシリーズを削除",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした",
"deleted_media_successfully": "他のメディアを削除しました!", "deleted_media_successfully": "他のメディアを削除しました!",
"failed_to_delete_media": "他のメディアの削除に失敗しました", "failed_to_delete_media": "他のメディアの削除に失敗しました",
"download_deleted": "ダウンロードが削除されました",
"download_cancelled": "ダウンロードをキャンセルしました", "download_cancelled": "ダウンロードをキャンセルしました",
"could_not_delete_download": "ダウンロードを削除できませんでした", "could_not_delete_download": "ダウンロードを削除できませんでした",
"download_paused": "ダウンロードを一時停止しました",
"could_not_pause_download": "ダウンロードを一時停止できませんでした",
"download_resumed": "ダウンロード再開",
"could_not_resume_download": "ダウンロードを再開できませんでした",
"download_completed": "ダウンロードが完了しました", "download_completed": "ダウンロードが完了しました",
"download_failed": "ダウンロードに失敗しました", "download_failed": "ダウンロードに失敗しました",
"download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました",
"failed_to_clean_cache_directory": "キャッシュディレクトリのクリーンアップに失敗しました",
"could_not_get_download_url_for_item": "{{itemName}} のダウンロードURLを取得できませんでした", "could_not_get_download_url_for_item": "{{itemName}} のダウンロードURLを取得できませんでした",
"go_to_downloads": "ダウンロードに移動",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "検索...", "search": "検索...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした",
"message_from_server": "サーバーからのメッセージ", "message_from_server": "サーバーからのメッセージ",
"next_episode": "次のエピソード", "next_episode": "次のエピソード",
"refresh_tracks": "トラックを更新",
"audio_tracks": "音声トラック:",
"playback_state": "再生状態:",
"index": "インデックス:",
"continue_watching": "視聴を続ける", "continue_watching": "視聴を続ける",
"go_back": "戻る", "go_back": "戻る",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "もっと見る", "show_more": "もっと見る",
"show_less": "少なく表示", "show_less": "少なく表示",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "이어서 보기 & 다음 시청", "continue_and_next_up": "이어서 보기 & 다음 시청",
"recently_added_in": "최근에 추가된 {{libraryName}}", "recently_added_in": "최근에 추가된 {{libraryName}}",
"suggested_movies": "추천 영화", "suggested_movies": "추천 영화",
"suggested_episodes": "추천 에피소드",
"intro": { "intro": {
"welcome_to_streamyfin": "스트리미핀에 오신 것을 환영합니다", "welcome_to_streamyfin": "스트리미핀에 오신 것을 환영합니다",
"a_free_and_open_source_client_for_jellyfin": "젤리핀을 위한 무료 오픈소스 클라이언트입니다.", "a_free_and_open_source_client_for_jellyfin": "젤리핀을 위한 무료 오픈소스 클라이언트입니다.",
@@ -261,43 +260,6 @@
"None": "None", "None": "None",
"OnlyForced": "OnlyForced" "OnlyForced": "OnlyForced"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "검정색",
"Gray": "회색",
"Silver": "은색",
"White": "흰색",
"Maroon": "밤색",
"Red": "빨간색",
"Fuchsia": "분홍색",
"Yellow": "노란색",
"Olive": "올리브 색",
"Green": "녹색",
"Teal": "청록색",
"Lime": "라임색",
"Purple": "보라색",
"Navy": "남색",
"Blue": "파란색",
"Aqua": "아쿠아색"
},
"thickness": {
"None": "없음",
"Thin": "얇게",
"Normal": "보통",
"Thick": "굵게"
},
"subtitle_color": "자막 색상",
"subtitle_background_color": "배경 색상",
"subtitle_font": "자막 폰트",
"ksplayer_title": "KSPlayer 설정",
"hardware_decode": "하드웨어 디코딩",
"hardware_decode_description": "비디오 디코딩에 하드웨어 가속을 사용하십시오. 재생 문제가 발생하는 경우 비활성화하십시오.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC 자막 설정",
"hint": "VLC 플레이어의 자막 표시 방식을 설정하세요. 변경 사항은 다음 재생 시 적용됩니다.",
"text_color": "글자색",
"background_color": "배경 색상",
"background_opacity": "배경 투명도",
"outline_color": "외곽선 색상",
"outline_opacity": "외곽선 투명도",
"outline_thickness": "외곽선 굵기",
"bold": "굵은 글씨",
"margin": "아래쪽 여백"
},
"video_player": {
"title": "비디오 플레이어",
"video_player": "비디오 플레이어",
"video_player_description": "iOS 사용자는 비디오 플레이어를 선택하세요.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Other", "other_title": "Other",
"video_orientation": "Video Orientation", "video_orientation": "Video Orientation",
@@ -351,13 +294,7 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "컨트롤 안전 영역", "safe_area_in_controls": "컨트롤 안전 영역",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "사용자 지정 메뉴 링크 표시", "show_custom_menu_links": "사용자 지정 메뉴 링크 표시",
"show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)",
"hide_libraries": "라이브러리 숨기기", "hide_libraries": "라이브러리 숨기기",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback", "disable_haptic_feedback": "Disable Haptic Feedback",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Read More About Marlin.", "read_more_about_marlin": "Read More About Marlin.",
"save_button": "Save", "save_button": "Save",
"toasts": { "toasts": {
"saved": "Saved", "saved": "Saved"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"enable_movie_recommendations": "Movie Recommendations", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "시리즈 추천", "enable_series_recommendations": "시리즈 추천",
"enable_promoted_watchlists": "추천 관심 목록", "enable_promoted_watchlists": "추천 관심 목록",
@@ -445,8 +374,7 @@
"refresh_from_server": "서버에서 설정 새로고침" "refresh_from_server": "서버에서 설정 새로고침"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "관심 목록 통합 기능 활성화", "watchlist_enabler": "관심 목록 통합 기능 활성화"
"watchlist_button": "관심 목록 연동 켜기/끄기"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Delete All Downloaded Files", "delete_all_downloaded_files": "Delete All Downloaded Files",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "음악 캐시가 삭제되었습니다", "music_cache_cleared": "음악 캐시가 삭제되었습니다",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export Logs", "export_logs": "Export Logs",
"click_for_more_info": "Click for More Info", "click_for_more_info": "Click for More Info",
"level": "Level", "level": "Level",
"no_logs_available": "No Logs Available", "no_logs_available": "No Logs Available"
"delete_all_logs": "Delete All Logs"
}, },
"languages": { "languages": {
"title": "Languages", "title": "Languages",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-Series", "tvseries": "TV-Series",
"movies": "Movies", "movies": "Movies",
"queue": "Queue",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Queue and downloads will be lost on app restart",
"no_items_in_queue": "No Items in Queue",
"no_downloaded_items": "No Downloaded Items", "no_downloaded_items": "No Downloaded Items",
"delete_all_movies_button": "Delete All Movies", "delete_all_movies_button": "Delete All Movies",
"delete_all_tvseries_button": "Delete All TV-Series", "delete_all_tvseries_button": "Delete All TV-Series",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Download Completed", "download_completed": "Download Completed",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}", "download_failed_for_item": "Download failed for {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode", "next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching", "continue_watching": "Continue Watching",
"go_back": "Go Back", "go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "Neste", "next_up": "Neste",
"recently_added_in": "Nylig lagt til i {{libraryName}}", "recently_added_in": "Nylig lagt til i {{libraryName}}",
"suggested_movies": "Foreslåtte filmer", "suggested_movies": "Foreslåtte filmer",
"suggested_episodes": "Foreslåtte episoder",
"intro": { "intro": {
"welcome_to_streamyfin": "Velkommen til Streamyfin", "welcome_to_streamyfin": "Velkommen til Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "En gratis og åpen kildekode-klient for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "En gratis og åpen kildekode-klient for Jellyfin.",
@@ -128,11 +127,6 @@
"UNKNOWN": "Ukjent" "UNKNOWN": "Ukjent"
}, },
"safe_area_in_controls": "Trygt område i kontrollene", "safe_area_in_controls": "Trygt område i kontrollene",
"video_player": "Videospiller",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimentell + PiP)"
},
"show_custom_menu_links": "Vis Tilpassede Meny Linker", "show_custom_menu_links": "Vis Tilpassede Meny Linker",
"hide_libraries": "Skjul biblioteker", "hide_libraries": "Skjul biblioteker",
"select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule fra Bibliotek-fanen og hjemmesidedelene.", "select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule fra Bibliotek-fanen og hjemmesidedelene.",
@@ -140,7 +134,6 @@
"default_quality": "Standardkvalitet" "default_quality": "Standardkvalitet"
}, },
"downloads": { "downloads": {
"downloads_title": "Nedlastinger",
"optimized_versions_server": "Optimaliserte versjoner server", "optimized_versions_server": "Optimaliserte versjoner server",
"save_button": "Lagre", "save_button": "Lagre",
"optimized_server": "Optimalisert Server", "optimized_server": "Optimalisert Server",
@@ -205,8 +198,7 @@
"export_logs": "Eksporter logger", "export_logs": "Eksporter logger",
"click_for_more_info": "Klikk for mer informasjon", "click_for_more_info": "Klikk for mer informasjon",
"level": "Nivå", "level": "Nivå",
"no_logs_available": "Ingen logger tilgjengelig", "no_logs_available": "Ingen logger tilgjengelig"
"delete_all_logs": "Slett alle logger"
}, },
"languages": { "languages": {
"title": "Språk", "title": "Språk",
@@ -216,8 +208,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "Feil ved sletting av filer", "error_deleting_files": "Feil ved sletting av filer",
"background_downloads_enabled": "Bakgrunnsnedlastinger aktivert",
"background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert",
"connected": "Tilkoblet", "connected": "Tilkoblet",
"could_not_connect": "Kunne ikke koble til", "could_not_connect": "Kunne ikke koble til",
"invalid_url": "Ugyldig URL" "invalid_url": "Ugyldig URL"
@@ -231,9 +221,6 @@
"downloads_title": "Nedlastinger", "downloads_title": "Nedlastinger",
"tvseries": "TV-serier", "tvseries": "TV-serier",
"movies": "Filmer", "movies": "Filmer",
"queue": "Kø",
"queue_hint": "Kø og nedlastinger vil gå tapt ved omstart av appen",
"no_items_in_queue": "Ingen elementer i køen",
"no_downloaded_items": "Ingen nedlastede elementer", "no_downloaded_items": "Ingen nedlastede elementer",
"delete_all_movies_button": "Slett alle filmer", "delete_all_movies_button": "Slett alle filmer",
"delete_all_tvseries_button": "Slett alle TV-serier", "delete_all_tvseries_button": "Slett alle TV-serier",
@@ -269,9 +256,7 @@
"no_response_received_from_server": "Ingen respons mottatt fra serveren", "no_response_received_from_server": "Ingen respons mottatt fra serveren",
"error_setting_up_the_request": "Feil under oppsett av forespørselen", "error_setting_up_the_request": "Feil under oppsett av forespørselen",
"failed_to_start_download_for_item_unexpected_error": "Kunne ikke starte nedlasting for {{item}}: Uventet feil", "failed_to_start_download_for_item_unexpected_error": "Kunne ikke starte nedlasting for {{item}}: Uventet feil",
"all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobber ble slettet", "an_error_occured_while_deleting_files_and_jobs": "En feil oppstod under sletting av filer og jobber"
"an_error_occured_while_deleting_files_and_jobs": "En feil oppstod under sletting av filer og jobber",
"go_to_downloads": "Gå til nedlastinger"
} }
} }
}, },
@@ -365,12 +350,8 @@
"video_has_finished_playing": "Videoen har avsluttet avspilling!", "video_has_finished_playing": "Videoen har avsluttet avspilling!",
"no_video_source": "Ingen videosource...", "no_video_source": "Ingen videosource...",
"next_episode": "Neste episode", "next_episode": "Neste episode",
"refresh_tracks": "Oppdater spor",
"subtitle_tracks": "Undertekstspor:", "subtitle_tracks": "Undertekstspor:",
"audio_tracks": "Lydspor:", "no_data_available": "Ingen data tilgjengelig"
"playback_state": "Avspillingsstatus:",
"no_data_available": "Ingen data tilgjengelig",
"index": "Indeks:"
}, },
"item_card": { "item_card": {
"next_up": "Neste opp", "next_up": "Neste opp",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Doorgaan & Volgende", "continue_and_next_up": "Doorgaan & Volgende",
"recently_added_in": "Recent toegevoegd in {{libraryName}}", "recently_added_in": "Recent toegevoegd in {{libraryName}}",
"suggested_movies": "Voorgestelde films", "suggested_movies": "Voorgestelde films",
"suggested_episodes": "Voorgestelde Afleveringen",
"intro": { "intro": {
"welcome_to_streamyfin": "Welkom bij Streamyfin", "welcome_to_streamyfin": "Welkom bij Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Geen", "None": "Geen",
"OnlyForced": "Alleen Geforceerd" "OnlyForced": "Alleen Geforceerd"
}, },
"text_color": "Tekst kleur",
"background_color": "Achtergrond Kleur",
"outline_color": "Kleur omlijning",
"outline_thickness": "Dikte omlijning",
"background_opacity": "Transparantie achtergrond",
"outline_opacity": "Doorzichtigheid omlijning",
"bold_text": "Bold Text",
"colors": {
"Black": "Zwart",
"Gray": "Grijs",
"Silver": "Zilver",
"White": "Wit",
"Maroon": "Kastanjebruin",
"Red": "Rood",
"Fuchsia": "Fuchsia",
"Yellow": "Geel",
"Olive": "Olijf",
"Green": "Groen",
"Teal": "Groenblauw",
"Lime": "Lichtgroen",
"Purple": "Paars",
"Navy": "Marine",
"Blue": "Blauw",
"Aqua": "Aqua"
},
"thickness": {
"None": "Geen",
"Thin": "Dun",
"Normal": "normaal",
"Thick": "Dikke"
},
"subtitle_color": "Kleur ondertiteling",
"subtitle_background_color": "Achtergrondkleur",
"subtitle_font": "Lettertype ondertitels",
"ksplayer_title": "KSPlayer Instellingen",
"hardware_decode": "Hardware Acceleratie",
"hardware_decode_description": "Gebruik hardware acceleratie voor video-decodering. Uitschakelen als u problemen met afspelen ondervindt.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC ondertitel instellingen",
"hint": "Aanpassen van ondertiteling voor VLC-speler. Wijzigingen worden toegepast bij het afspelen.",
"text_color": "Tekstkleur",
"background_color": "Achtergrondkleur",
"background_opacity": "Doorzichtigheid achtergrond",
"outline_color": "Kleur omlijning",
"outline_opacity": "Omtrek opaciteit",
"outline_thickness": "Omtrek dikte",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Videospeler",
"video_player": "Videospeler",
"video_player_description": "Kies welke videospeler gebruikt moet worden op iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Andere", "other_title": "Andere",
"video_orientation": "Video oriëntatie", "video_orientation": "Video oriëntatie",
@@ -351,13 +294,7 @@
"UNKNOWN": "Onbekend" "UNKNOWN": "Onbekend"
}, },
"safe_area_in_controls": "Veilig gebied in bedieningen", "safe_area_in_controls": "Veilig gebied in bedieningen",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimentele + PiP)"
},
"show_custom_menu_links": "Aangepaste menulinks tonen", "show_custom_menu_links": "Aangepaste menulinks tonen",
"show_large_home_carousel": "Toon grote carrousel op startpagina (bèta)",
"hide_libraries": "Verberg Bibliotheken", "hide_libraries": "Verberg Bibliotheken",
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
"disable_haptic_feedback": "Haptische feedback uitschakelen", "disable_haptic_feedback": "Haptische feedback uitschakelen",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Automatisch Aflevering Aantal", "max_auto_play_episode_count": "Max Automatisch Aflevering Aantal",
"disabled": "Uitgeschakeld" "disabled": "Uitgeschakeld"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Muziek", "title": "Muziek",
"playback_title": "Afspelen", "playback_title": "Afspelen",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Lees meer over Marlin.", "read_more_about_marlin": "Lees meer over Marlin.",
"save_button": "Opslaan", "save_button": "Opslaan",
"toasts": { "toasts": {
"saved": "Opgeslagen", "saved": "Opgeslagen"
"refreshed": "Instellingen zijn vernieuwd vanaf server" }
},
"refresh_from_server": "Ververs Instellingen van Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Streamystats inschakelen",
"disable_streamystats": "Streamystats Uitschakelen", "disable_streamystats": "Streamystats Uitschakelen",
"enable_search": "Gebruik voor Zoeken", "enable_search": "Gebruik voor Zoeken",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Vul de URL van de Streamystats server in. De URL moet http of https bevatten en optioneel de poort.", "streamystats_search_hint": "Vul de URL van de Streamystats server in. De URL moet http of https bevatten en optioneel de poort.",
"read_more_about_streamystats": "Lees Meer over Streamystats.", "read_more_about_streamystats": "Lees Meer over Streamystats.",
"save_button": "Opslaan",
"save": "Opslaan", "save": "Opslaan",
"features_title": "Functies", "features_title": "Functies",
"home_sections_title": "Thuis Secties",
"enable_movie_recommendations": "Film Aanbevelingen", "enable_movie_recommendations": "Film Aanbevelingen",
"enable_series_recommendations": "Series Aanbevelingen", "enable_series_recommendations": "Series Aanbevelingen",
"enable_promoted_watchlists": "Gepromote Kijklijst", "enable_promoted_watchlists": "Gepromote Kijklijst",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Verwijder alle gedownloade bestanden", "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} gecached", "music_cache_size": "{{size}} gecached",
"music_cache_cleared": "Muziek cache gewist", "music_cache_cleared": "Muziek cache gewist",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Klik voor meer info", "click_for_more_info": "Klik voor meer info",
"level": "Niveau", "level": "Niveau",
"no_logs_available": "Geen logs beschikbaar", "no_logs_available": "Geen logs beschikbaar"
"delete_all_logs": "Alle logs verwijderen"
}, },
"languages": { "languages": {
"title": "Talen", "title": "Talen",
@@ -490,15 +414,12 @@
"system": "Systeem" "system": "Systeem"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Fout bij het verwijderen van bestanden", "error_deleting_files": "Fout bij het verwijderen van bestanden"
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "Series", "tvseries": "Series",
"movies": "Films", "movies": "Films",
"queue": "Wachtrij",
"other_media": "Andere media", "other_media": "Andere media",
"queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app",
"no_items_in_queue": "Geen items in wachtrij",
"no_downloaded_items": "Geen gedownloade items", "no_downloaded_items": "Geen gedownloade items",
"delete_all_movies_button": "Verwijder alle films", "delete_all_movies_button": "Verwijder alle films",
"delete_all_tvseries_button": "Verwijder alle Series", "delete_all_tvseries_button": "Verwijder alle Series",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
"deleted_media_successfully": "Andere media succesvol verwijderd!", "deleted_media_successfully": "Andere media succesvol verwijderd!",
"failed_to_delete_media": "Verwijderen van andere media mislukt", "failed_to_delete_media": "Verwijderen van andere media mislukt",
"download_deleted": "Download verwijderd",
"download_cancelled": "Download geannuleerd", "download_cancelled": "Download geannuleerd",
"could_not_delete_download": "Kon download niet verwijderen", "could_not_delete_download": "Kon download niet verwijderen",
"download_paused": "Download gepauzeerd",
"could_not_pause_download": "Kan niet pauzeren download",
"download_resumed": "Download hervat",
"could_not_resume_download": "Kon de download niet hervatten",
"download_completed": "Download afgerond", "download_completed": "Download afgerond",
"download_failed": "Download Mislukt", "download_failed": "Download Mislukt",
"download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} wordt al gedownload", "item_already_downloading": "{{item}} wordt al gedownload",
"all_files_deleted": "Alle Bestanden Succesvol Gedownload", "all_files_deleted": "Alle Bestanden Succesvol Gedownload",
"files_deleted_by_type": "{{count}} {{type}} verwijderd", "files_deleted_by_type": "{{count}} {{type}} verwijderd",
"all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd",
"failed_to_clean_cache_directory": "Opschonen cachemap mislukt",
"could_not_get_download_url_for_item": "Kan download-URL voor {{itemName}} niet ophalen", "could_not_get_download_url_for_item": "Kan download-URL voor {{itemName}} niet ophalen",
"go_to_downloads": "Ga naar downloads",
"file_deleted": "{{item}} verwijderd" "file_deleted": "{{item}} verwijderd"
} }
} }
@@ -583,16 +493,13 @@
"none": "Geen", "none": "Geen",
"track": "Spoor", "track": "Spoor",
"cancel": "Annuleren", "cancel": "Annuleren",
"stop": "Stop",
"delete": "Verwijderen", "delete": "Verwijderen",
"ok": "Oké", "ok": "Oké",
"remove": "Verwijderen", "remove": "Verwijderen",
"next": "Volgende",
"back": "Terug", "back": "Terug",
"continue": "Doorgaan", "continue": "Doorgaan",
"verifying": "Verifiëren...", "verifying": "Verifiëren...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Zoek...", "search": "Zoek...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
"message_from_server": "Bericht van de server", "message_from_server": "Bericht van de server",
"next_episode": "Volgende Aflevering", "next_episode": "Volgende Aflevering",
"refresh_tracks": "Tracks verversen",
"audio_tracks": "Audio Tracks:",
"playback_state": "Afspeelstatus:",
"index": "Index:",
"continue_watching": "Verder kijken", "continue_watching": "Verder kijken",
"go_back": "Terug", "go_back": "Terug",
"downloaded_file_title": "Je hebt dit bestand gedownload", "downloaded_file_title": "Je hebt dit bestand gedownload",
@@ -761,7 +664,6 @@
"show_more": "Toon meer", "show_more": "Toon meer",
"show_less": "Toon minder", "show_less": "Toon minder",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Afspeellijsten", "playlists": "Afspeellijsten",
"tracks": "Nummers" "tracks": "Nummers"
}, },
"filters": {
"all": "Alle"
},
"recently_added": "Recent toegevoegd", "recently_added": "Recent toegevoegd",
"recently_played": "Onlangs afgespeeld", "recently_played": "Onlangs afgespeeld",
"frequently_played": "Vaak afgespeeld", "frequently_played": "Vaak afgespeeld",
"explore": "Ontdek",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Afspelen", "play": "Afspelen",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "Neste", "next_up": "Neste",
"recently_added_in": "Nyleg lagt til i {{libraryName}}", "recently_added_in": "Nyleg lagt til i {{libraryName}}",
"suggested_movies": "Foreslåtte filmar", "suggested_movies": "Foreslåtte filmar",
"suggested_episodes": "Foreslåtte episodar",
"intro": { "intro": {
"welcome_to_streamyfin": "Velkommen til Streamyfin", "welcome_to_streamyfin": "Velkommen til Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Ein gratis og open kjeldekode-klient for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Ein gratis og open kjeldekode-klient for Jellyfin.",
@@ -128,11 +127,6 @@
"UNKNOWN": "Ukjent" "UNKNOWN": "Ukjent"
}, },
"safe_area_in_controls": "Trygt område i kontrollane", "safe_area_in_controls": "Trygt område i kontrollane",
"video_player": "Videospelar",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimentell + PiP)"
},
"show_custom_menu_links": "Vis Tilpassede Meny Linker", "show_custom_menu_links": "Vis Tilpassede Meny Linker",
"hide_libraries": "Skjul bibliotek", "hide_libraries": "Skjul bibliotek",
"select_liraries_you_want_to_hide": "Vel biblioteka du vil skjula frå Bibliotek-fanen og nettsidedelane.", "select_liraries_you_want_to_hide": "Vel biblioteka du vil skjula frå Bibliotek-fanen og nettsidedelane.",
@@ -140,7 +134,6 @@
"default_quality": "Standardkvalitet" "default_quality": "Standardkvalitet"
}, },
"downloads": { "downloads": {
"downloads_title": "Nedlastingar",
"optimized_versions_server": "Optimaliserte versjonar servar", "optimized_versions_server": "Optimaliserte versjonar servar",
"save_button": "Lagre", "save_button": "Lagre",
"optimized_server": "Optimalisert Servar", "optimized_server": "Optimalisert Servar",
@@ -205,8 +198,7 @@
"export_logs": "Eksporter loggar", "export_logs": "Eksporter loggar",
"click_for_more_info": "Klikk for meir informasjon", "click_for_more_info": "Klikk for meir informasjon",
"level": "Nivå", "level": "Nivå",
"no_logs_available": "Ingen loggar tilgjengelege", "no_logs_available": "Ingen loggar tilgjengelege"
"delete_all_logs": "Slett alle loggar"
}, },
"languages": { "languages": {
"title": "Språk", "title": "Språk",
@@ -216,8 +208,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "Feil ved sletting av filer", "error_deleting_files": "Feil ved sletting av filer",
"background_downloads_enabled": "Bakgrunnsnedlastingar aktiverte",
"background_downloads_disabled": "Bakgrunnsnedlastingar deaktiverte",
"connected": "Tilkopla", "connected": "Tilkopla",
"could_not_connect": "Kunne ikkje kopla til", "could_not_connect": "Kunne ikkje kopla til",
"invalid_url": "Ugyldig URL" "invalid_url": "Ugyldig URL"
@@ -231,9 +221,6 @@
"downloads_title": "Nedlastingar", "downloads_title": "Nedlastingar",
"tvseries": "TV-seriar", "tvseries": "TV-seriar",
"movies": "Filmar", "movies": "Filmar",
"queue": "Kø",
"queue_hint": "Kø og nedlastingar vil gå tapt ved omstart av appen",
"no_items_in_queue": "Ingen element i køen",
"no_downloaded_items": "Ingen nedlasta element", "no_downloaded_items": "Ingen nedlasta element",
"delete_all_movies_button": "Slett alle filmar", "delete_all_movies_button": "Slett alle filmar",
"delete_all_tvseries_button": "Slett alle TV-seriar", "delete_all_tvseries_button": "Slett alle TV-seriar",
@@ -269,9 +256,7 @@
"no_response_received_from_server": "Ingen respons motteken frå serveren", "no_response_received_from_server": "Ingen respons motteken frå serveren",
"error_setting_up_the_request": "Feil under oppsett av førespurnaden", "error_setting_up_the_request": "Feil under oppsett av førespurnaden",
"failed_to_start_download_for_item_unexpected_error": "Kunne ikkje starta nedlasting for {{item}}: Uventa feil", "failed_to_start_download_for_item_unexpected_error": "Kunne ikkje starta nedlasting for {{item}}: Uventa feil",
"all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobbar vart sletta", "an_error_occured_while_deleting_files_and_jobs": "Ein feil oppstod under sletting av filer og jobbar"
"an_error_occured_while_deleting_files_and_jobs": "Ein feil oppstod under sletting av filer og jobbar",
"go_to_downloads": "Gå til nedlastingar"
} }
} }
}, },
@@ -365,12 +350,8 @@
"video_has_finished_playing": "Videoen er ferdig avspelt!", "video_has_finished_playing": "Videoen er ferdig avspelt!",
"no_video_source": "Ingen videokjelde...", "no_video_source": "Ingen videokjelde...",
"next_episode": "Neste episode", "next_episode": "Neste episode",
"refresh_tracks": "Oppdater spor",
"subtitle_tracks": "Undertekstspor:", "subtitle_tracks": "Undertekstspor:",
"audio_tracks": "Lydspor:", "no_data_available": "Ingen data tilgjengelege"
"playback_state": "Avspelingstatus:",
"no_data_available": "Ingen data tilgjengelege",
"index": "Indeks:"
}, },
"item_card": { "item_card": {
"next_up": "Neste opp", "next_up": "Neste opp",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Nylig lagt til i {{libraryName}}", "recently_added_in": "Nylig lagt til i {{libraryName}}",
"suggested_movies": "Foreslåtte filmer", "suggested_movies": "Foreslåtte filmer",
"suggested_episodes": "Foreslåtte episoder",
"intro": { "intro": {
"welcome_to_streamyfin": "Velkommen til Streamyfin", "welcome_to_streamyfin": "Velkommen til Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "En gratis og Open-Source klient for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "En gratis og Open-Source klient for Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Ingen", "None": "Ingen",
"OnlyForced": "Enkelt" "OnlyForced": "Enkelt"
}, },
"text_color": "Tekst farge",
"background_color": "Bakgrunnsfarge",
"outline_color": "Omrissets farge",
"outline_thickness": "Omriss Tykkelse",
"background_opacity": "Bakgrunns gjennomsiktighet",
"outline_opacity": "Omrissets gjennomsiktighet",
"bold_text": "Bold Text",
"colors": {
"Black": "Svart",
"Gray": "Grå",
"Silver": "Sølv",
"White": "Hvit",
"Maroon": "Rødbrun",
"Red": "Rød",
"Fuchsia": "Fuchsia",
"Yellow": "Gul",
"Olive": "Olivengrønn",
"Green": "Grønn",
"Teal": "Blågrønn",
"Lime": "Limegrønn",
"Purple": "Lilla",
"Navy": "Marineblå",
"Blue": "Blå",
"Aqua": "Vann"
},
"thickness": {
"None": "Ingen",
"Thin": "Tynn",
"Normal": "Vanlig",
"Thick": "Tykk"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Annet", "other_title": "Annet",
"video_orientation": "Video Retning", "video_orientation": "Video Retning",
@@ -351,13 +294,7 @@
"UNKNOWN": "Ukjent" "UNKNOWN": "Ukjent"
}, },
"safe_area_in_controls": "Sikker sone i kontroller", "safe_area_in_controls": "Sikker sone i kontroller",
"video_player": "Video Spiller",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (eksperimentell + PiP)"
},
"show_custom_menu_links": "Vis tilpassede menylenker", "show_custom_menu_links": "Vis tilpassede menylenker",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Skjul biblioteker", "hide_libraries": "Skjul biblioteker",
"select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule deg for Biblioteket og avsnittene for hjemmesider.", "select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule deg for Biblioteket og avsnittene for hjemmesider.",
"disable_haptic_feedback": "Deaktiver Haptisk tilbakemelding", "disable_haptic_feedback": "Deaktiver Haptisk tilbakemelding",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maks automatisk avspilling Episode Telling", "max_auto_play_episode_count": "Maks automatisk avspilling Episode Telling",
"disabled": "Deaktivert" "disabled": "Deaktivert"
}, },
"downloads": {
"downloads_title": "Nedlastinger"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Les mer om Marlin.", "read_more_about_marlin": "Les mer om Marlin.",
"save_button": "Lagre", "save_button": "Lagre",
"toasts": { "toasts": {
"saved": "Lagret", "saved": "Lagret"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Slett alle nedlastede filer", "delete_all_downloaded_files": "Slett alle nedlastede filer",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Eksportlogger", "export_logs": "Eksportlogger",
"click_for_more_info": "Klikk for mer info", "click_for_more_info": "Klikk for mer info",
"level": "Nivå", "level": "Nivå",
"no_logs_available": "Ingen logger tilgjengelig", "no_logs_available": "Ingen logger tilgjengelig"
"delete_all_logs": "Slett alle loggene"
}, },
"languages": { "languages": {
"title": "Språk", "title": "Språk",
@@ -490,15 +414,12 @@
"system": "Systemadministrasjon" "system": "Systemadministrasjon"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Feil ved sletting av filer", "error_deleting_files": "Feil ved sletting av filer"
"background_downloads_enabled": "Nedlastinger av bakgrunn aktivert",
"background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Nedlastinger", "downloads_title": "Nedlastinger",
"tvseries": "TV-Serier", "tvseries": "TV-Serier",
"movies": "Filmer", "movies": "Filmer",
"queue": "Kø",
"other_media": "Andre medier", "other_media": "Andre medier",
"queue_hint": "Kø og nedlastinger vil gå tapt når appen startes på nytt",
"no_items_in_queue": "Ingen elementer i køen",
"no_downloaded_items": "Ingen nedlastede elementer", "no_downloaded_items": "Ingen nedlastede elementer",
"delete_all_movies_button": "Slett alle filmer", "delete_all_movies_button": "Slett alle filmer",
"delete_all_tvseries_button": "Slett alle TV-Serier", "delete_all_tvseries_button": "Slett alle TV-Serier",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-Serier", "failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-Serier",
"deleted_media_successfully": "Slettet andre media vellykket!", "deleted_media_successfully": "Slettet andre media vellykket!",
"failed_to_delete_media": "Kunne ikke slette andre medier", "failed_to_delete_media": "Kunne ikke slette andre medier",
"download_deleted": "Nedlasting slettet",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Kunne ikke slette nedlasting", "could_not_delete_download": "Kunne ikke slette nedlasting",
"download_paused": "Last ned Pauset",
"could_not_pause_download": "Kunne ikke pause nedlasting",
"download_resumed": "Nedlastingen er gjenopptatt",
"could_not_resume_download": "Kunne ikke fortsette nedlasting",
"download_completed": "Nedlasting fullført", "download_completed": "Nedlasting fullført",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Nedlasting feilet for {{item}} {{error}}", "download_failed_for_item": "Nedlasting feilet for {{item}} {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobber slettet",
"failed_to_clean_cache_directory": "Klarte ikke å tømme mellomlagermappen",
"could_not_get_download_url_for_item": "Kunne ikke hente nedlastings-URL for {{itemName}}", "could_not_get_download_url_for_item": "Kunne ikke hente nedlastings-URL for {{itemName}}",
"go_to_downloads": "Gå til nedlastinger",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Søk...", "search": "Søk...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Kan ikke opprette en strøm for Chromecast", "could_not_create_stream_for_chromecast": "Kan ikke opprette en strøm for Chromecast",
"message_from_server": "Melding fra tjener: {{message}}", "message_from_server": "Melding fra tjener: {{message}}",
"next_episode": "Neste Episode", "next_episode": "Neste Episode",
"refresh_tracks": "Oppdater sporing",
"audio_tracks": "Lyd Tracks:",
"playback_state": "Avspillingsstatus:",
"index": "Indeks:",
"continue_watching": "Fortsett å se", "continue_watching": "Fortsett å se",
"go_back": "Gå tilbake", "go_back": "Gå tilbake",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Vis mer", "show_more": "Vis mer",
"show_less": "Vis mindre", "show_less": "Vis mindre",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Oglądaj dalej i Następne", "continue_and_next_up": "Oglądaj dalej i Następne",
"recently_added_in": "Ostatnio dodano w {{libraryName}}", "recently_added_in": "Ostatnio dodano w {{libraryName}}",
"suggested_movies": "Sugerowane filmy", "suggested_movies": "Sugerowane filmy",
"suggested_episodes": "Sugerowane odcinki",
"intro": { "intro": {
"welcome_to_streamyfin": "Witamy w Streamyfin", "welcome_to_streamyfin": "Witamy w Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Darmowy i otwartoźródłowy klient dla Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Darmowy i otwartoźródłowy klient dla Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Brak", "None": "Brak",
"OnlyForced": "Tylko wymuszone" "OnlyForced": "Tylko wymuszone"
}, },
"text_color": "Kolor tekstu",
"background_color": "Kolor tła",
"outline_color": "Kolor konturu",
"outline_thickness": "Grubość konturu",
"background_opacity": "Przezroczystość tła",
"outline_opacity": "Przezroczystość konturu",
"bold_text": "Tekst pogrubiony",
"colors": {
"Black": "Czarny",
"Gray": "Szary",
"Silver": "Srebro",
"White": "Biały",
"Maroon": "Bordowy",
"Red": "Czerwony",
"Fuchsia": "Fuksja",
"Yellow": "Żółty",
"Olive": "Oliwki",
"Green": "Zielony",
"Teal": "Turkusowy",
"Lime": "Limonkowy",
"Purple": "Fioletowy",
"Navy": "Granatowy",
"Blue": "Niebieski",
"Aqua": "Aqua"
},
"thickness": {
"None": "Brak",
"Thin": "Cienka",
"Normal": "Normalny",
"Thick": "Gruba"
},
"subtitle_color": "Kolor napisów",
"subtitle_background_color": "Kolor tła",
"subtitle_font": "Czcionka napisów",
"ksplayer_title": "Ustawienia KSPlayer",
"hardware_decode": "Dekodowanie sprzętowe",
"hardware_decode_description": "Używaj akceleracji sprzętowej dla dekodowania wideo. Wyłącz, jeśli doświadczasz problemów z odtwarzaniem.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "Ustawienia napisów VLC",
"hint": "Personalizuj wygląd napisów dla odtwarzacza VLC. Zmiany zajdą przy następnym odtwarzaniu.",
"text_color": "Kolor tekstu",
"background_color": "Kolor tła",
"background_opacity": "Przezroczystość tła",
"outline_color": "Kolor obrysu",
"outline_opacity": "Przezroczystość obrysu",
"outline_thickness": "Grubość obrysu",
"bold": "Pogrubiony tekst",
"margin": "Dolny margines"
},
"video_player": {
"title": "Odtwarzacz wideo",
"video_player": "Odtwarzacz wideo",
"video_player_description": "Wybierz którego odtwarzacza wideo używać w iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Inne", "other_title": "Inne",
"video_orientation": "Orientacja wideo", "video_orientation": "Orientacja wideo",
@@ -351,13 +294,7 @@
"UNKNOWN": "Nieznana" "UNKNOWN": "Nieznana"
}, },
"safe_area_in_controls": "Bezpieczny obszar w kontrolkach", "safe_area_in_controls": "Bezpieczny obszar w kontrolkach",
"video_player": "Odtwarzacz wideo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperymentalny + PiP)"
},
"show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu", "show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu",
"show_large_home_carousel": "Wyświetl Dużą Karuzelę na ekranie głównym (beta)",
"hide_libraries": "Ukryj biblioteki", "hide_libraries": "Ukryj biblioteki",
"select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.", "select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
"disable_haptic_feedback": "Wyłącz wibracje", "disable_haptic_feedback": "Wyłącz wibracje",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maksymalna liczba odcinków automatycznego odtwarzania", "max_auto_play_episode_count": "Maksymalna liczba odcinków automatycznego odtwarzania",
"disabled": "Wyłączone" "disabled": "Wyłączone"
}, },
"downloads": {
"downloads_title": "Pobieranie"
},
"music": { "music": {
"title": "Muzyka", "title": "Muzyka",
"playback_title": "Odtwarzanie", "playback_title": "Odtwarzanie",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Dowiedz się więcej o Marlin.", "read_more_about_marlin": "Dowiedz się więcej o Marlin.",
"save_button": "Zapisz", "save_button": "Zapisz",
"toasts": { "toasts": {
"saved": "Zapisano", "saved": "Zapisano"
"refreshed": "Ustawienia odświeżone z serwera" }
},
"refresh_from_server": "Odśwież ustawienia z serwera"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Włącz Streamystats",
"disable_streamystats": "Wyłącz Streamystats", "disable_streamystats": "Wyłącz Streamystats",
"enable_search": "Używaj do wyszukiwania", "enable_search": "Używaj do wyszukiwania",
"url": "Adres URL", "url": "Adres URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Wprowadź adres URL dla twojego serwera Streamystats. URL powinien zawierać http lub https i opcjonalnie port.", "streamystats_search_hint": "Wprowadź adres URL dla twojego serwera Streamystats. URL powinien zawierać http lub https i opcjonalnie port.",
"read_more_about_streamystats": "Dowiedz się więcej o Streamystats.", "read_more_about_streamystats": "Dowiedz się więcej o Streamystats.",
"save_button": "Zapisz",
"save": "Zapisz", "save": "Zapisz",
"features_title": "Funkcje", "features_title": "Funkcje",
"home_sections_title": "Sekcja główna",
"enable_movie_recommendations": "Rekomendacje filmów", "enable_movie_recommendations": "Rekomendacje filmów",
"enable_series_recommendations": "Rekomendację seriali", "enable_series_recommendations": "Rekomendację seriali",
"enable_promoted_watchlists": "Promowane listy oglądania", "enable_promoted_watchlists": "Promowane listy oglądania",
@@ -445,8 +374,7 @@
"refresh_from_server": "Odśwież ustawienia z serwera" "refresh_from_server": "Odśwież ustawienia z serwera"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Aktywuj naszą integrację Listy Oglądania", "watchlist_enabler": "Aktywuj naszą integrację Listy Oglądania"
"watchlist_button": "Przelącz integrację Listy Oglądania"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Usuń wszystkie pobrane pliki", "delete_all_downloaded_files": "Usuń wszystkie pobrane pliki",
"music_cache_title": "Bufor muzyki", "music_cache_title": "Bufor muzyki",
"music_cache_description": "Automatycznie buforuj piosenki w trakcie słuchania dla płynniejszego odtwarzania i wsparcia offline", "music_cache_description": "Automatycznie buforuj piosenki w trakcie słuchania dla płynniejszego odtwarzania i wsparcia offline",
"enable_music_cache": "Włącz bufor muzyki",
"clear_music_cache": "Wyczyść bufor muzyki", "clear_music_cache": "Wyczyść bufor muzyki",
"music_cache_size": "Zbuforowano {{size}}", "music_cache_size": "Zbuforowano {{size}}",
"music_cache_cleared": "Wyczyszczono bufor muzyki", "music_cache_cleared": "Wyczyszczono bufor muzyki",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Eksportuj logi", "export_logs": "Eksportuj logi",
"click_for_more_info": "Kliknij po więcej informacji", "click_for_more_info": "Kliknij po więcej informacji",
"level": "Poziom", "level": "Poziom",
"no_logs_available": "Brak dostępnych logów", "no_logs_available": "Brak dostępnych logów"
"delete_all_logs": "Usuń wszystkie logi"
}, },
"languages": { "languages": {
"title": "Języki", "title": "Języki",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Błąd podczas usuwania plików", "error_deleting_files": "Błąd podczas usuwania plików"
"background_downloads_enabled": "Pobieranie w tle włączone",
"background_downloads_disabled": "Pobieranie w tle wyłączone"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Pobrane", "downloads_title": "Pobrane",
"tvseries": "Seriale", "tvseries": "Seriale",
"movies": "Filmy", "movies": "Filmy",
"queue": "Kolejka",
"other_media": "Inne media", "other_media": "Inne media",
"queue_hint": "Kolejka i pobierania zostaną utracone po ponownym uruchomieniu aplikacji",
"no_items_in_queue": "Brak elementów w kolejce",
"no_downloaded_items": "Brak pobranych elementów", "no_downloaded_items": "Brak pobranych elementów",
"delete_all_movies_button": "Usuń wszystkie filmy", "delete_all_movies_button": "Usuń wszystkie filmy",
"delete_all_tvseries_button": "Usuń wszystkie seriale", "delete_all_tvseries_button": "Usuń wszystkie seriale",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali", "failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali",
"deleted_media_successfully": "Pomyślnie usunięto inne media!", "deleted_media_successfully": "Pomyślnie usunięto inne media!",
"failed_to_delete_media": "Nie udało się usunąć innych mediów", "failed_to_delete_media": "Nie udało się usunąć innych mediów",
"download_deleted": "Pobieranie usunięte",
"download_cancelled": "Pobieranie anulowane", "download_cancelled": "Pobieranie anulowane",
"could_not_delete_download": "Nie można usunąć pobrania", "could_not_delete_download": "Nie można usunąć pobrania",
"download_paused": "Pobieranie wstrzymane",
"could_not_pause_download": "Nie można wstrzymać pobierania",
"download_resumed": "Pobieranie wznowione",
"could_not_resume_download": "Nie można wznowić pobierania",
"download_completed": "Pobieranie zakończone", "download_completed": "Pobieranie zakończone",
"download_failed": "Pobieranie nie powiodło się", "download_failed": "Pobieranie nie powiodło się",
"download_failed_for_item": "Pobieranie nie powiodło się dla {{item}} {{error}}", "download_failed_for_item": "Pobieranie nie powiodło się dla {{item}} {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} jest w trakcie pobierania", "item_already_downloading": "{{item}} jest w trakcie pobierania",
"all_files_deleted": "Pomyślnie usunięto wszystkie pobrane", "all_files_deleted": "Pomyślnie usunięto wszystkie pobrane",
"files_deleted_by_type": "{{count}} {{type}} usunięto", "files_deleted_by_type": "{{count}} {{type}} usunięto",
"all_files_folders_and_jobs_deleted_successfully": "Wszystkie pliki, foldery i zadania zostały pomyślnie usunięte",
"failed_to_clean_cache_directory": "Nie udało się wyczyścić katalogu pamięci podręcznej",
"could_not_get_download_url_for_item": "Nie można pobrać adresu URL dla {{itemName}}", "could_not_get_download_url_for_item": "Nie można pobrać adresu URL dla {{itemName}}",
"go_to_downloads": "Przejdź do pobranych",
"file_deleted": "Usunięto {{item}}" "file_deleted": "Usunięto {{item}}"
} }
} }
@@ -583,16 +493,13 @@
"none": "Nic", "none": "Nic",
"track": "Utwór", "track": "Utwór",
"cancel": "Anuluj", "cancel": "Anuluj",
"stop": "Stop",
"delete": "Usuń", "delete": "Usuń",
"ok": "OK", "ok": "OK",
"remove": "Usuń", "remove": "Usuń",
"next": "Następne",
"back": "Poprzednie", "back": "Poprzednie",
"continue": "Kontynuuj", "continue": "Kontynuuj",
"verifying": "Weryfikacja...", "verifying": "Weryfikacja...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Szukaj...", "search": "Szukaj...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta", "could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta",
"message_from_server": "Wiadomość z serwera: {{message}}", "message_from_server": "Wiadomość z serwera: {{message}}",
"next_episode": "Następny odcinek", "next_episode": "Następny odcinek",
"refresh_tracks": "Odśwież ścieżki",
"audio_tracks": "Ścieżki audio:",
"playback_state": "Stan odtwarzania:",
"index": "Indeks:",
"continue_watching": "Kontynuuj oglądanie", "continue_watching": "Kontynuuj oglądanie",
"go_back": "Wstecz", "go_back": "Wstecz",
"downloaded_file_title": "Ten plik masz już pobrany", "downloaded_file_title": "Ten plik masz już pobrany",
@@ -761,7 +664,6 @@
"show_more": "Pokaż więcej", "show_more": "Pokaż więcej",
"show_less": "Pokaż mniej", "show_less": "Pokaż mniej",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlisty", "playlists": "Playlisty",
"tracks": "utwory" "tracks": "utwory"
}, },
"filters": {
"all": "Wszystkie"
},
"recently_added": "Ostatnio dodano", "recently_added": "Ostatnio dodano",
"recently_played": "Ostatnio odtwarzano", "recently_played": "Ostatnio odtwarzano",
"frequently_played": "Często odtwarzane", "frequently_played": "Często odtwarzane",
"explore": "Odkrywaj",
"top_tracks": "Popularne utwory", "top_tracks": "Popularne utwory",
"play": "Odtwórz", "play": "Odtwórz",
"shuffle": "Losuj", "shuffle": "Losuj",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "Próximo em", "next_up": "Próximo em",
"recently_added_in": "Adicionados recentemente em {{libraryName}}", "recently_added_in": "Adicionados recentemente em {{libraryName}}",
"suggested_movies": "Filmes sugeridos", "suggested_movies": "Filmes sugeridos",
"suggested_episodes": "Episódios sugeridos",
"intro": { "intro": {
"welcome_to_streamyfin": "Bem-vindo ao Streamyfin", "welcome_to_streamyfin": "Bem-vindo ao Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Um cliente gratuito e de código livre para o Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Um cliente gratuito e de código livre para o Jellyfin.",
@@ -128,11 +127,6 @@
"UNKNOWN": "Desconhecido" "UNKNOWN": "Desconhecido"
}, },
"safe_area_in_controls": "Área segura nos controles", "safe_area_in_controls": "Área segura nos controles",
"video_player": "Tocador de vídeo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Mostrar Custom Links no Menu", "show_custom_menu_links": "Mostrar Custom Links no Menu",
"hide_libraries": "Ocultar bibliotecas", "hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.", "select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.",
@@ -141,7 +135,6 @@
"disabled": "Desativado" "disabled": "Desativado"
}, },
"downloads": { "downloads": {
"downloads_title": "Downloads",
"optimized_versions_server": "Servidor do optimized versions", "optimized_versions_server": "Servidor do optimized versions",
"save_button": "Salvar", "save_button": "Salvar",
"optimized_server": "Optimized Server", "optimized_server": "Optimized Server",
@@ -203,8 +196,7 @@
}, },
"logs": { "logs": {
"logs_title": "Logs", "logs_title": "Logs",
"no_logs_available": "Sem logs disponíveis", "no_logs_available": "Sem logs disponíveis"
"delete_all_logs": "Remover todos os logs"
}, },
"languages": { "languages": {
"title": "Idiomas", "title": "Idiomas",
@@ -214,8 +206,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "Erro ao remover arquivos", "error_deleting_files": "Erro ao remover arquivos",
"background_downloads_enabled": "Downloads em segundo plano ativado",
"background_downloads_disabled": "Downloads em segundo plano desativado",
"connected": "Conectado", "connected": "Conectado",
"could_not_connect": "Não foi possível conectar", "could_not_connect": "Não foi possível conectar",
"invalid_url": "URL inválida" "invalid_url": "URL inválida"
@@ -229,9 +219,6 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV/Séries", "tvseries": "TV/Séries",
"movies": "Filmes", "movies": "Filmes",
"queue": "Fila",
"queue_hint": "A fila e os downloads serão perdidos ao reiniciar o aplicativo",
"no_items_in_queue": "Nenhum item na fila",
"no_downloaded_items": "Nenhum item baixado", "no_downloaded_items": "Nenhum item baixado",
"delete_all_movies_button": "Remover todos os filmes", "delete_all_movies_button": "Remover todos os filmes",
"delete_all_tvseries_button": "Remover todos as TV/Séries", "delete_all_tvseries_button": "Remover todos as TV/Séries",
@@ -267,9 +254,7 @@
"no_response_received_from_server": "Sem resposta recebida do servidor", "no_response_received_from_server": "Sem resposta recebida do servidor",
"error_setting_up_the_request": "Erro ao configurar a solicitação", "error_setting_up_the_request": "Erro ao configurar a solicitação",
"failed_to_start_download_for_item_unexpected_error": "Falha ao iniciar o download de {{item}}: Erro inesperado", "failed_to_start_download_for_item_unexpected_error": "Falha ao iniciar o download de {{item}}: Erro inesperado",
"all_files_folders_and_jobs_deleted_successfully": "Todos arquivos, pastas e jobs removidos com sucesso", "an_error_occured_while_deleting_files_and_jobs": "Ocorreu um erro ao remover os arquivos e jobs"
"an_error_occured_while_deleting_files_and_jobs": "Ocorreu um erro ao remover os arquivos e jobs",
"go_to_downloads": "Vá para downloads"
} }
} }
}, },
@@ -363,12 +348,8 @@
"video_has_finished_playing": "O vídeo terminou!", "video_has_finished_playing": "O vídeo terminou!",
"no_video_source": "Nenhuma fonte de vídeo...", "no_video_source": "Nenhuma fonte de vídeo...",
"next_episode": "Próximo episódio", "next_episode": "Próximo episódio",
"refresh_tracks": "Atualizar faixas",
"subtitle_tracks": "Faixas da legenda:", "subtitle_tracks": "Faixas da legenda:",
"audio_tracks": "Faixas do áudio:",
"playback_state": "Playback State:",
"no_data_available": "Nenhum dado disponível", "no_data_available": "Nenhum dado disponível",
"index": "Índice:",
"continue_watching": "Continuar assistindo", "continue_watching": "Continuar assistindo",
"go_back": "Voltar" "go_back": "Voltar"
}, },

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continuar e Próximo", "continue_and_next_up": "Continuar e Próximo",
"recently_added_in": "Adicionado recentemente em {{libraryName}}", "recently_added_in": "Adicionado recentemente em {{libraryName}}",
"suggested_movies": "Filmes Sugeridos", "suggested_movies": "Filmes Sugeridos",
"suggested_episodes": "Episódios sugeridos",
"intro": { "intro": {
"welcome_to_streamyfin": "Bem-vindo ao Streamyfin", "welcome_to_streamyfin": "Bem-vindo ao Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Um Cliente de código aberto e gratuito para o Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Um Cliente de código aberto e gratuito para o Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Nenhuma", "None": "Nenhuma",
"OnlyForced": "Somente Forçado" "OnlyForced": "Somente Forçado"
}, },
"text_color": "Cor do texto",
"background_color": "Cor de fundo",
"outline_color": "Cor do contorno",
"outline_thickness": "Espessura do Contorno",
"background_opacity": "Opacidade de fundo",
"outline_opacity": "Opacidade do Contorno",
"bold_text": "Texto em negrito",
"colors": {
"Black": "Preto",
"Gray": "Cinzento",
"Silver": "Prata",
"White": "Branco",
"Maroon": "Castanho",
"Red": "Vermelho",
"Fuchsia": "Fuchsia",
"Yellow": "Amarelo",
"Olive": "Verde-oliva",
"Green": "Verde",
"Teal": "Verde-azulado",
"Lime": "Verde-limão",
"Purple": "Roxo",
"Navy": "Azul-marinho",
"Blue": "Azul",
"Aqua": "Água"
},
"thickness": {
"None": "Nenhuma",
"Thin": "Magro",
"Normal": "Normal",
"Thick": "Grosso"
},
"subtitle_color": "Cor da legenda",
"subtitle_background_color": "Cor de fundo",
"subtitle_font": "Fonte da legenda",
"ksplayer_title": "Configurações do KSPlayer",
"hardware_decode": "Decodificação por hardware",
"hardware_decode_description": "Use aceleração de hardware para decodificação de vídeo. Desative se você tiver problemas de reprodução.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Reprodutor de Vídeo",
"video_player": "Reprodutor de Vídeo",
"video_player_description": "Escolha qual player de vídeo usar no iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Outros", "other_title": "Outros",
"video_orientation": "Orientação do Vídeo", "video_orientation": "Orientação do Vídeo",
@@ -351,13 +294,7 @@
"UNKNOWN": "Desconhecido" "UNKNOWN": "Desconhecido"
}, },
"safe_area_in_controls": "Área segura nos controles", "safe_area_in_controls": "Área segura nos controles",
"video_player": "Reprodutor de Vídeo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Mostrar Links de Menu Personalizado", "show_custom_menu_links": "Mostrar Links de Menu Personalizado",
"show_large_home_carousel": "Mostrar Carrossel Grande (beta)",
"hide_libraries": "Ocultar bibliotecas", "hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar da aba Biblioteca e seções da página inicial.", "select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar da aba Biblioteca e seções da página inicial.",
"disable_haptic_feedback": "Desativar o retorno tátil", "disable_haptic_feedback": "Desativar o retorno tátil",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Contagem máxima de episódios de reprodução automática", "max_auto_play_episode_count": "Contagem máxima de episódios de reprodução automática",
"disabled": "Desabilitado" "disabled": "Desabilitado"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Música", "title": "Música",
"playback_title": "Reproduzir", "playback_title": "Reproduzir",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Leia mais sobre Marlin.", "read_more_about_marlin": "Leia mais sobre Marlin.",
"save_button": "Salvar", "save_button": "Salvar",
"toasts": { "toasts": {
"saved": "Salvo", "saved": "Salvo"
"refreshed": "Configurações atualizadas do servidor" }
},
"refresh_from_server": "Atualizar as configurações do servidor"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Ativar Streamystats",
"disable_streamystats": "Desativar streamystats", "disable_streamystats": "Desativar streamystats",
"enable_search": "Usar para Pesquisa", "enable_search": "Usar para Pesquisa",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Digite a URL para seu servidor de StreamyStats. A URL deve incluir http ou https e, opcionalmente, a porta.", "streamystats_search_hint": "Digite a URL para seu servidor de StreamyStats. A URL deve incluir http ou https e, opcionalmente, a porta.",
"read_more_about_streamystats": "Leia mais sobre Streamystats.", "read_more_about_streamystats": "Leia mais sobre Streamystats.",
"save_button": "Salvar",
"save": "Salvar", "save": "Salvar",
"features_title": "Funcionalidades", "features_title": "Funcionalidades",
"home_sections_title": "Seções da Página Inicial",
"enable_movie_recommendations": "Recomendações de filmes", "enable_movie_recommendations": "Recomendações de filmes",
"enable_series_recommendations": "Recomendações de Séries", "enable_series_recommendations": "Recomendações de Séries",
"enable_promoted_watchlists": "Promoted Watchlists", "enable_promoted_watchlists": "Promoted Watchlists",
@@ -445,8 +374,7 @@
"refresh_from_server": "Atualizar Configurações do Servidor" "refresh_from_server": "Atualizar Configurações do Servidor"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Ative nossa integração de Lista de Interesses", "watchlist_enabler": "Ative nossa integração de Lista de Interesses"
"watchlist_button": "Ativar/desativar Lista de Interesses"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Excluir todos os arquivos baixados", "delete_all_downloaded_files": "Excluir todos os arquivos baixados",
"music_cache_title": "Cache de Música", "music_cache_title": "Cache de Música",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Habilitar Cache de Música",
"clear_music_cache": "Limpar Cache de Música", "clear_music_cache": "Limpar Cache de Música",
"music_cache_size": "{{size}} em cache", "music_cache_size": "{{size}} em cache",
"music_cache_cleared": "Cache de música limpo", "music_cache_cleared": "Cache de música limpo",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Exportar logs", "export_logs": "Exportar logs",
"click_for_more_info": "Clique para mais informações", "click_for_more_info": "Clique para mais informações",
"level": "Nível", "level": "Nível",
"no_logs_available": "Não há registros disponíveis", "no_logs_available": "Não há registros disponíveis"
"delete_all_logs": "Excluir todos os registros"
}, },
"languages": { "languages": {
"title": "Idiomas", "title": "Idiomas",
@@ -490,15 +414,12 @@
"system": "Sistema" "system": "Sistema"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Erro ao excluir arquivos", "error_deleting_files": "Erro ao excluir arquivos"
"background_downloads_enabled": "Downloads em segundo plano ativados",
"background_downloads_disabled": "Downloads em segundo plano desativados"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-Séries", "tvseries": "TV-Séries",
"movies": "Filmes", "movies": "Filmes",
"queue": "Fila",
"other_media": "Outras mídias", "other_media": "Outras mídias",
"queue_hint": "A fila e os downloads serão perdidos ao reiniciar o aplicativo",
"no_items_in_queue": "Nenhum item na fila",
"no_downloaded_items": "Nenhum item baixado", "no_downloaded_items": "Nenhum item baixado",
"delete_all_movies_button": "Excluir todos os filmes", "delete_all_movies_button": "Excluir todos os filmes",
"delete_all_tvseries_button": "Excluir todas as séries", "delete_all_tvseries_button": "Excluir todas as séries",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Falha ao excluir todas as séries", "failed_to_delete_all_tvseries": "Falha ao excluir todas as séries",
"deleted_media_successfully": "Outras mídias excluídas com sucesso!", "deleted_media_successfully": "Outras mídias excluídas com sucesso!",
"failed_to_delete_media": "Falha ao excluir outras mídias", "failed_to_delete_media": "Falha ao excluir outras mídias",
"download_deleted": "Download Excluído",
"download_cancelled": "Download Cancelado", "download_cancelled": "Download Cancelado",
"could_not_delete_download": "Não foi possível excluir o download", "could_not_delete_download": "Não foi possível excluir o download",
"download_paused": "Download Pausado",
"could_not_pause_download": "Não foi possível Pausar o Download",
"download_resumed": "Download Retomado",
"could_not_resume_download": "Não foi possível retomar o download",
"download_completed": "Download concluído", "download_completed": "Download concluído",
"download_failed": "Download Falhou", "download_failed": "Download Falhou",
"download_failed_for_item": "Download Falhou para {{item}} - {{error}}", "download_failed_for_item": "Download Falhou para {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} já está sendo baixado", "item_already_downloading": "{{item}} já está sendo baixado",
"all_files_deleted": "Todos os Downloads Excluídos com Sucesso", "all_files_deleted": "Todos os Downloads Excluídos com Sucesso",
"files_deleted_by_type": "{{count}} {{type}} excluído", "files_deleted_by_type": "{{count}} {{type}} excluído",
"all_files_folders_and_jobs_deleted_successfully": "Todos os arquivos, pastas e trabalhos excluídos com sucesso",
"failed_to_clean_cache_directory": "Falha ao limpar o diretório de cache",
"could_not_get_download_url_for_item": "Não foi possível obter o URL de download para {{itemName}}", "could_not_get_download_url_for_item": "Não foi possível obter o URL de download para {{itemName}}",
"go_to_downloads": "Ir para Downloads",
"file_deleted": "{{item}} deletado" "file_deleted": "{{item}} deletado"
} }
} }
@@ -583,16 +493,13 @@
"none": "Nenhum", "none": "Nenhum",
"track": "Faixa", "track": "Faixa",
"cancel": "Cancelar", "cancel": "Cancelar",
"stop": "Stop",
"delete": "Apagar", "delete": "Apagar",
"ok": "OK", "ok": "OK",
"remove": "Remover", "remove": "Remover",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Buscar...", "search": "Buscar...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Não foi possível criar um fluxo para o Chromecast", "could_not_create_stream_for_chromecast": "Não foi possível criar um fluxo para o Chromecast",
"message_from_server": "Mensagem do Servidor: {{message}}", "message_from_server": "Mensagem do Servidor: {{message}}",
"next_episode": "Próximo Episódio", "next_episode": "Próximo Episódio",
"refresh_tracks": "Atualizar Faixas",
"audio_tracks": "Faixas de Áudio:",
"playback_state": "Estado de Reprodução:",
"index": "Índice",
"continue_watching": "Continuar assistindo", "continue_watching": "Continuar assistindo",
"go_back": "Voltar atrás", "go_back": "Voltar atrás",
"downloaded_file_title": "Você já fez o download deste arquivo", "downloaded_file_title": "Você já fez o download deste arquivo",
@@ -761,7 +664,6 @@
"show_more": "Mostrar mais", "show_more": "Mostrar mais",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "faixas" "tracks": "faixas"
}, },
"filters": {
"all": "Tudo"
},
"recently_added": "Adicionado recentemente", "recently_added": "Adicionado recentemente",
"recently_played": "Reproduzido Recentemente", "recently_played": "Reproduzido Recentemente",
"frequently_played": "Reproduzidos com frequência", "frequently_played": "Reproduzidos com frequência",
"explore": "Explorar",
"top_tracks": "Músicas populares", "top_tracks": "Músicas populares",
"play": "Reproduzir", "play": "Reproduzir",
"shuffle": "Alteatório", "shuffle": "Alteatório",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Adăugat recent în {{libraryName}}", "recently_added_in": "Adăugat recent în {{libraryName}}",
"suggested_movies": "Filme sugerate", "suggested_movies": "Filme sugerate",
"suggested_episodes": "Episoade sugerate",
"intro": { "intro": {
"welcome_to_streamyfin": "Bun venit la Streamyfin", "welcome_to_streamyfin": "Bun venit la Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Un client gratuit și open-source pentru Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Un client gratuit și open-source pentru Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Niciuna", "None": "Niciuna",
"OnlyForced": "OnlyForced" "OnlyForced": "OnlyForced"
}, },
"text_color": "Culoare text",
"background_color": "Culoare fundal",
"outline_color": "Culoare contur",
"outline_thickness": "Grosime contur",
"background_opacity": "Opacitatea fundalului",
"outline_opacity": "Opacitatea conturului",
"bold_text": "Bold Text",
"colors": {
"Black": "Negru",
"Gray": "Gri",
"Silver": "Argint",
"White": "Alb",
"Maroon": "Maro",
"Red": "Roșu",
"Fuchsia": "Fuchsia",
"Yellow": "Galben",
"Olive": "Oliv",
"Green": "Verde",
"Teal": "Turcoaz",
"Lime": "Verde-Deschis",
"Purple": "Violet",
"Navy": "Marină",
"Blue": "Albastru",
"Aqua": "Aqua"
},
"thickness": {
"None": "Nimic",
"Thin": "Subțire",
"Normal": "Normală",
"Thick": "Grozav"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Altele", "other_title": "Altele",
"video_orientation": "Orientarea video", "video_orientation": "Orientarea video",
@@ -351,13 +294,7 @@
"UNKNOWN": "Necunoscut" "UNKNOWN": "Necunoscut"
}, },
"safe_area_in_controls": "Zona sigură pentru controale", "safe_area_in_controls": "Zona sigură pentru controale",
"video_player": "Player video",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Afișează link-uri personalizate în meniu", "show_custom_menu_links": "Afișează link-uri personalizate în meniu",
"show_large_home_carousel": "Arată Caruselul Media Mare (beta)",
"hide_libraries": "Ascunde bibliotecile", "hide_libraries": "Ascunde bibliotecile",
"select_liraries_you_want_to_hide": "Selectează bibliotecile pe care dorești să le ascunzi din fila Bibliotecă și din secțiunile paginii principale.", "select_liraries_you_want_to_hide": "Selectează bibliotecile pe care dorești să le ascunzi din fila Bibliotecă și din secțiunile paginii principale.",
"disable_haptic_feedback": "Dezactivează vibrațiile tactile", "disable_haptic_feedback": "Dezactivează vibrațiile tactile",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Maxim episoade redare automată", "max_auto_play_episode_count": "Maxim episoade redare automată",
"disabled": "Dezactivat" "disabled": "Dezactivat"
}, },
"downloads": {
"downloads_title": "Descărcări"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Citește mai multe despre Marlin.", "read_more_about_marlin": "Citește mai multe despre Marlin.",
"save_button": "Salvează", "save_button": "Salvează",
"toasts": { "toasts": {
"saved": "Salvat", "saved": "Salvat"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Ștergeți toate fișierele descărcate", "delete_all_downloaded_files": "Ștergeți toate fișierele descărcate",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export loguri", "export_logs": "Export loguri",
"click_for_more_info": "Apasă pt mai multe informații", "click_for_more_info": "Apasă pt mai multe informații",
"level": "Nivel", "level": "Nivel",
"no_logs_available": "Niciun log disponibil", "no_logs_available": "Niciun log disponibil"
"delete_all_logs": "Șterge toate logurile"
}, },
"languages": { "languages": {
"title": "Limbi", "title": "Limbi",
@@ -490,15 +414,12 @@
"system": "Sistem" "system": "Sistem"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Eroare la ștergerea fișierelor", "error_deleting_files": "Eroare la ștergerea fișierelor"
"background_downloads_enabled": "Descărcări în fundal activate",
"background_downloads_disabled": "Descărcări în fundal dezactivate"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Descărcări", "downloads_title": "Descărcări",
"tvseries": "Seriale", "tvseries": "Seriale",
"movies": "Filme", "movies": "Filme",
"queue": "Coadă",
"other_media": "Alte suporturi", "other_media": "Alte suporturi",
"queue_hint": "Descărcările se vor pierde la repornirea aplicației",
"no_items_in_queue": "Niciun articol în coadă",
"no_downloaded_items": "Niciun element descărcat", "no_downloaded_items": "Niciun element descărcat",
"delete_all_movies_button": "Șterge toate filmele", "delete_all_movies_button": "Șterge toate filmele",
"delete_all_tvseries_button": "Șterge toate serialele", "delete_all_tvseries_button": "Șterge toate serialele",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Nu s-au putut șterge toate serialele", "failed_to_delete_all_tvseries": "Nu s-au putut șterge toate serialele",
"deleted_media_successfully": "Alte fișiere șterse cu succes!", "deleted_media_successfully": "Alte fișiere șterse cu succes!",
"failed_to_delete_media": "Ștergerea altor fișiere media a eșuat", "failed_to_delete_media": "Ștergerea altor fișiere media a eșuat",
"download_deleted": "Descărcare ştearsă",
"download_cancelled": "Descărcare anulată", "download_cancelled": "Descărcare anulată",
"could_not_delete_download": "Nu s-a putut șterge descărcarea", "could_not_delete_download": "Nu s-a putut șterge descărcarea",
"download_paused": "Descărcare întreruptă",
"could_not_pause_download": "Nu s-a putut întrerupe descărcarea",
"download_resumed": "Descărcare din nou",
"could_not_resume_download": "Nu s-a putut relua descărcarea",
"download_completed": "Descărcare completă", "download_completed": "Descărcare completă",
"download_failed": "Descărcare eșuată", "download_failed": "Descărcare eșuată",
"download_failed_for_item": "Descărcarea a eșuat {{item}} - {{error}}", "download_failed_for_item": "Descărcarea a eșuat {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} se descarcă deja", "item_already_downloading": "{{item}} se descarcă deja",
"all_files_deleted": "Toate descărcările au fost șterse cu succes", "all_files_deleted": "Toate descărcările au fost șterse cu succes",
"files_deleted_by_type": "{{count}} {{type}} au fost șterse", "files_deleted_by_type": "{{count}} {{type}} au fost șterse",
"all_files_folders_and_jobs_deleted_successfully": "Toate fișierele, folderele și lucrările au fost șterse cu succes",
"failed_to_clean_cache_directory": "Curățarea directorului cache a eșuat",
"could_not_get_download_url_for_item": "Nu s-a putut obține URL-ul de descărcare pentru {{itemName}}", "could_not_get_download_url_for_item": "Nu s-a putut obține URL-ul de descărcare pentru {{itemName}}",
"go_to_downloads": "Accesați descărcările",
"file_deleted": "{{item}} șters" "file_deleted": "{{item}} șters"
} }
} }
@@ -583,16 +493,13 @@
"none": "Nimic", "none": "Nimic",
"track": "Limbă audio", "track": "Limbă audio",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Caută...", "search": "Caută...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Nu s-a putut crea un flux pentru Chromecast", "could_not_create_stream_for_chromecast": "Nu s-a putut crea un flux pentru Chromecast",
"message_from_server": "Mesaj de la server: {{message}}", "message_from_server": "Mesaj de la server: {{message}}",
"next_episode": "Episodul următor", "next_episode": "Episodul următor",
"refresh_tracks": "Reîmprospătare piese",
"audio_tracks": "Audio:",
"playback_state": "Stare de redare:",
"index": "Indice:",
"continue_watching": "Continuă să vizionezi", "continue_watching": "Continuă să vizionezi",
"go_back": "Înapoi", "go_back": "Înapoi",
"downloaded_file_title": "Aveţi acest fişier descărcat", "downloaded_file_title": "Aveţi acest fişier descărcat",
@@ -761,7 +664,6 @@
"show_more": "Arată mai mult", "show_more": "Arată mai mult",
"show_less": "Arată mai puțin", "show_less": "Arată mai puțin",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Продолжить и Далее", "continue_and_next_up": "Продолжить и Далее",
"recently_added_in": "Недавно добавлено в {{libraryName}}", "recently_added_in": "Недавно добавлено в {{libraryName}}",
"suggested_movies": "Предложенные фильмы", "suggested_movies": "Предложенные фильмы",
"suggested_episodes": "Предложенные серии",
"intro": { "intro": {
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin", "welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом.", "a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом.",
@@ -261,43 +260,6 @@
"None": "Отсутствует", "None": "Отсутствует",
"OnlyForced": "Только принудительные" "OnlyForced": "Только принудительные"
}, },
"text_color": "Цвет текста",
"background_color": "Цвет фона",
"outline_color": "Цвет контура",
"outline_thickness": "Толщина контура",
"background_opacity": "Прозрачность фона",
"outline_opacity": "Прозрачность контура",
"bold_text": "Жирный",
"colors": {
"Black": "Черный",
"Gray": "Серый",
"Silver": "Серебристый",
"White": "Белый",
"Maroon": "Бордовый",
"Red": "Красный",
"Fuchsia": "Пурпурный",
"Yellow": "Жёлтый",
"Olive": "Оливковый",
"Green": "Зелёный",
"Teal": "Бирюзовый",
"Lime": "Лаймовый",
"Purple": "Фиолетовый",
"Navy": "Тёмно-синий",
"Blue": "Синий",
"Aqua": "Голубой"
},
"thickness": {
"None": "Отсутствует",
"Thin": "Тонкий",
"Normal": "Обычный",
"Thick": "Толстый"
},
"subtitle_color": "Цвет субтитров",
"subtitle_background_color": "Цвет фона",
"subtitle_font": "Шрифт субтитров",
"ksplayer_title": "Настройки KSPlayer",
"hardware_decode": "Аппаратное декодирование",
"hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "Настройки субтитров в VLC",
"hint": "Настройте внешний вид субтитров в VLC плеере. Изменения применятся при следующем воспроизведении.",
"text_color": "Цвет текста",
"background_color": "Цвет фона",
"background_opacity": "Прозрачность фона",
"outline_color": "Цвет контура",
"outline_opacity": "Прозрачность контура",
"outline_thickness": "Толщина контура",
"bold": "Жирный",
"margin": "Отступ снизу"
},
"video_player": {
"title": "Видео плеер",
"video_player": "Видео плеер",
"video_player_description": "Выберите видео плеер в iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Другое", "other_title": "Другое",
"video_orientation": "Ориентация видео", "video_orientation": "Ориентация видео",
@@ -351,13 +294,7 @@
"UNKNOWN": "Неизвестное" "UNKNOWN": "Неизвестное"
}, },
"safe_area_in_controls": "Безопасная зона в элементах управления", "safe_area_in_controls": "Безопасная зона в элементах управления",
"video_player": "Видео плеер",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
},
"show_custom_menu_links": "Показать ссылки пользовательского меню", "show_custom_menu_links": "Показать ссылки пользовательского меню",
"show_large_home_carousel": "Показывать большую карусель (beta)",
"hide_libraries": "Скрыть библиотеки", "hide_libraries": "Скрыть библиотеки",
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.", "select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
"disable_haptic_feedback": "Отключить тактильную обратную связь", "disable_haptic_feedback": "Отключить тактильную обратную связь",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Максимальное количество авто воспроизводимых эпизодов", "max_auto_play_episode_count": "Максимальное количество авто воспроизводимых эпизодов",
"disabled": "Отключено" "disabled": "Отключено"
}, },
"downloads": {
"downloads_title": "Загрузки"
},
"music": { "music": {
"title": "Музыка", "title": "Музыка",
"playback_title": "Воспроизведение", "playback_title": "Воспроизведение",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Узнать больше о Marlin.", "read_more_about_marlin": "Узнать больше о Marlin.",
"save_button": "Сохранить", "save_button": "Сохранить",
"toasts": { "toasts": {
"saved": "Сохранено", "saved": "Сохранено"
"refreshed": "Настройки обновлены с сервера" }
},
"refresh_from_server": "Обновить настройки с сервера"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Включить Streamystats",
"disable_streamystats": "Выключить Streamystats", "disable_streamystats": "Выключить Streamystats",
"enable_search": "Использовать в поиске", "enable_search": "Использовать в поиске",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Введите URL вашего сервера Streamystats. URL должен включать http/https и порт при необходимости.", "streamystats_search_hint": "Введите URL вашего сервера Streamystats. URL должен включать http/https и порт при необходимости.",
"read_more_about_streamystats": "Узнать больше про Streamystats.", "read_more_about_streamystats": "Узнать больше про Streamystats.",
"save_button": "Сохранить",
"save": "Сохранить", "save": "Сохранить",
"features_title": "Функции", "features_title": "Функции",
"home_sections_title": "Показывать на главной",
"enable_movie_recommendations": "Рекомендации фильмов", "enable_movie_recommendations": "Рекомендации фильмов",
"enable_series_recommendations": "Рекомендации сериалов", "enable_series_recommendations": "Рекомендации сериалов",
"enable_promoted_watchlists": "Продвигаемые списки просмотра", "enable_promoted_watchlists": "Продвигаемые списки просмотра",
@@ -445,8 +374,7 @@
"refresh_from_server": "Обновить настройки с сервера" "refresh_from_server": "Обновить настройки с сервера"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Включить интеграцию со списками просмотра", "watchlist_enabler": "Включить интеграцию со списками просмотра"
"watchlist_button": "Изменить интеграцию со списками просмотра"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Удалить все загруженные файлы", "delete_all_downloaded_files": "Удалить все загруженные файлы",
"music_cache_title": "Кеш музыки", "music_cache_title": "Кеш музыки",
"music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета", "music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
"enable_music_cache": "Кешировать музыку",
"clear_music_cache": "Очистить кеш музыки", "clear_music_cache": "Очистить кеш музыки",
"music_cache_size": "Кешировано: {{size}}", "music_cache_size": "Кешировано: {{size}}",
"music_cache_cleared": "Кеш музыки очищен", "music_cache_cleared": "Кеш музыки очищен",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Сохранить логи", "export_logs": "Сохранить логи",
"click_for_more_info": "Нажмите для получения дополнительной информации", "click_for_more_info": "Нажмите для получения дополнительной информации",
"level": "Уровень", "level": "Уровень",
"no_logs_available": "Логи не доступны", "no_logs_available": "Логи не доступны"
"delete_all_logs": "Удалить все логи"
}, },
"languages": { "languages": {
"title": "Языки", "title": "Языки",
@@ -490,15 +414,12 @@
"system": "Системный" "system": "Системный"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Ошибка при удалении файлов", "error_deleting_files": "Ошибка при удалении файлов"
"background_downloads_enabled": "Фоновая загрузка включена",
"background_downloads_disabled": "Фоновая загрузка отключена"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Загрузки", "downloads_title": "Загрузки",
"tvseries": "Сериалы", "tvseries": "Сериалы",
"movies": "Фильмы", "movies": "Фильмы",
"queue": "Очередь",
"other_media": "Прочие файлы", "other_media": "Прочие файлы",
"queue_hint": "Очередь очистится после перезапуска",
"no_items_in_queue": "Нет элементов в очереди",
"no_downloaded_items": "Нет загруженных файлов", "no_downloaded_items": "Нет загруженных файлов",
"delete_all_movies_button": "Удалить все фильмы", "delete_all_movies_button": "Удалить все фильмы",
"delete_all_tvseries_button": "Удалить все сериалы", "delete_all_tvseries_button": "Удалить все сериалы",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов", "failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
"deleted_media_successfully": "Остальные медиафайлы успешно удалены!", "deleted_media_successfully": "Остальные медиафайлы успешно удалены!",
"failed_to_delete_media": "Не удалось удалить остальные медиафайлы", "failed_to_delete_media": "Не удалось удалить остальные медиафайлы",
"download_deleted": "Загруженный контент удалён",
"download_cancelled": "Загрузка отменена", "download_cancelled": "Загрузка отменена",
"could_not_delete_download": "Не удалось удалить загрузку", "could_not_delete_download": "Не удалось удалить загрузку",
"download_paused": "На паузе",
"could_not_pause_download": "Не удалось приостановить загрузку",
"download_resumed": "Продолжено",
"could_not_resume_download": "Не удалось возобновить загрузку",
"download_completed": "Завершено", "download_completed": "Завершено",
"download_failed": "Не удалось загрузить", "download_failed": "Не удалось загрузить",
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}", "download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} уже загружается", "item_already_downloading": "{{item}} уже загружается",
"all_files_deleted": "Все загрузки удалены", "all_files_deleted": "Все загрузки удалены",
"files_deleted_by_type": "Удалено: {{count}} {{type}}", "files_deleted_by_type": "Удалено: {{count}} {{type}}",
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
"failed_to_clean_cache_directory": "Не удалось очистить директорию кэша",
"could_not_get_download_url_for_item": "Не удалось получить URL для загрузки {{itemName}}", "could_not_get_download_url_for_item": "Не удалось получить URL для загрузки {{itemName}}",
"go_to_downloads": "В загрузки",
"file_deleted": "Удалено: {{item}}" "file_deleted": "Удалено: {{item}}"
} }
} }
@@ -583,16 +493,13 @@
"none": "Отсутствует", "none": "Отсутствует",
"track": "Трек", "track": "Трек",
"cancel": "Отмена", "cancel": "Отмена",
"stop": "Stop",
"delete": "Удалить", "delete": "Удалить",
"ok": "ОК", "ok": "ОК",
"remove": "Удалить", "remove": "Удалить",
"next": "Вперед",
"back": "Назад", "back": "Назад",
"continue": "Продолжить", "continue": "Продолжить",
"verifying": "Проверка...", "verifying": "Проверка...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Поиск...", "search": "Поиск...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast", "could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
"message_from_server": "Сообщение от сервера: {{message}}", "message_from_server": "Сообщение от сервера: {{message}}",
"next_episode": "Следующая серия", "next_episode": "Следующая серия",
"refresh_tracks": "Обновить дорожки",
"audio_tracks": "Аудио дорожки:",
"playback_state": "Состояние воспроизведения:",
"index": "Индекс:",
"continue_watching": "Продолжить просмотр", "continue_watching": "Продолжить просмотр",
"go_back": "Назад", "go_back": "Назад",
"downloaded_file_title": "Этот файл уже скачан", "downloaded_file_title": "Этот файл уже скачан",
@@ -761,7 +664,6 @@
"show_more": "Показать больше", "show_more": "Показать больше",
"show_less": "Показать меньше", "show_less": "Показать меньше",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Плейлисты", "playlists": "Плейлисты",
"tracks": "треки" "tracks": "треки"
}, },
"filters": {
"all": "Все"
},
"recently_added": "Недавно добавлено", "recently_added": "Недавно добавлено",
"recently_played": "Недавно воспроизведено", "recently_played": "Недавно воспроизведено",
"frequently_played": "Часто играет", "frequently_played": "Часто играет",
"explore": "Найти новое",
"top_tracks": "Топ", "top_tracks": "Топ",
"play": "Воспроизвести", "play": "Воспроизвести",
"shuffle": "Перемешать", "shuffle": "Перемешать",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "I ardhshëm", "next_up": "I ardhshëm",
"recently_added_in": "Shtuar kohët e fundit në {{libraryName}}", "recently_added_in": "Shtuar kohët e fundit në {{libraryName}}",
"suggested_movies": "Filma të sugjeruar", "suggested_movies": "Filma të sugjeruar",
"suggested_episodes": "Episodat të sugjeruara",
"intro": { "intro": {
"welcome_to_streamyfin": "Mirë se vini në Streamyfin", "welcome_to_streamyfin": "Mirë se vini në Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Një klient falas dhe me burim të hapur për Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Një klient falas dhe me burim të hapur për Jellyfin.",
@@ -128,11 +127,6 @@
"UNKNOWN": "E panjohur" "UNKNOWN": "E panjohur"
}, },
"safe_area_in_controls": "Zonë e sigurt në kontrolla", "safe_area_in_controls": "Zonë e sigurt në kontrolla",
"video_player": "Video lexues",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimentale + PiP)"
},
"show_custom_menu_links": "Shfaq lidhje menuje të personalizuara", "show_custom_menu_links": "Shfaq lidhje menuje të personalizuara",
"hide_libraries": "Fsheh bibliotekat", "hide_libraries": "Fsheh bibliotekat",
"select_liraries_you_want_to_hide": "Zgjidhni bibliotekat që dëshironi të fshehni nga skeda e Bibliotekut dhe seksionet e faqes kryesore.", "select_liraries_you_want_to_hide": "Zgjidhni bibliotekat që dëshironi të fshehni nga skeda e Bibliotekut dhe seksionet e faqes kryesore.",
@@ -140,7 +134,6 @@
"default_quality": "Kvaliteti standard" "default_quality": "Kvaliteti standard"
}, },
"downloads": { "downloads": {
"downloads_title": "Shkarkime",
"optimized_versions_server": "Serveri i versioneve të optimizuara", "optimized_versions_server": "Serveri i versioneve të optimizuara",
"save_button": "Ruaj", "save_button": "Ruaj",
"optimized_server": "Server i optimizuar", "optimized_server": "Server i optimizuar",
@@ -205,8 +198,7 @@
"export_logs": "Eksporto regjistrin", "export_logs": "Eksporto regjistrin",
"click_for_more_info": "Kliko për më shumë informacion", "click_for_more_info": "Kliko për më shumë informacion",
"level": "Nivele", "level": "Nivele",
"no_logs_available": "Nuk ka regjistrime të disponueshme", "no_logs_available": "Nuk ka regjistrime të disponueshme"
"delete_all_logs": "Fshijë të gjitha regjistrimet"
}, },
"languages": { "languages": {
"title": "Gjuhët", "title": "Gjuhët",
@@ -216,8 +208,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "Gabim gjatë fshirjes së skedarëve", "error_deleting_files": "Gabim gjatë fshirjes së skedarëve",
"background_downloads_enabled": "Shkarkimet në sfond aktivizuar",
"background_downloads_disabled": "Shkarkimet në sfond deaktivizuar",
"connected": "Lidhur", "connected": "Lidhur",
"could_not_connect": "Nuk u mundet te vendoset kyqja", "could_not_connect": "Nuk u mundet te vendoset kyqja",
"invalid_url": "URL i pavlefshme" "invalid_url": "URL i pavlefshme"
@@ -231,9 +221,6 @@
"downloads_title": "Shkarkimet", "downloads_title": "Shkarkimet",
"tvseries": "Seriale TV", "tvseries": "Seriale TV",
"movies": "Filma", "movies": "Filma",
"queue": "Rradhë",
"queue_hint": "Rradhat dhe shkarkimet do të humbasin pas genstartit të aplikacionit",
"no_items_in_queue": "Nuk ka elemente në rradhë",
"no_downloaded_items": "Nuk ka shkarkime", "no_downloaded_items": "Nuk ka shkarkime",
"delete_all_movies_button": "Fshijë të gjithë filmat", "delete_all_movies_button": "Fshijë të gjithë filmat",
"delete_all_tvseries_button": "Fshijë të gjitha serialet TV", "delete_all_tvseries_button": "Fshijë të gjitha serialet TV",
@@ -269,9 +256,7 @@
"no_response_received_from_server": "Nuk u mor asnjë përgjigje nga serveri", "no_response_received_from_server": "Nuk u mor asnjë përgjigje nga serveri",
"error_setting_up_the_request": "Gabim gjatë konfigurimit të kërkesës", "error_setting_up_the_request": "Gabim gjatë konfigurimit të kërkesës",
"failed_to_start_download_for_item_unexpected_error": "Dështoj fillimi i shkarkimit për {{item}}: Gabim i papritur", "failed_to_start_download_for_item_unexpected_error": "Dështoj fillimi i shkarkimit për {{item}}: Gabim i papritur",
"all_files_folders_and_jobs_deleted_successfully": "Të gjitha skedarët, dosjet dhe detyrat u fshinë me sukses", "an_error_occured_while_deleting_files_and_jobs": "Ndodhi një gabim gjatë fshirjes së skedarëve dhe detyrave"
"an_error_occured_while_deleting_files_and_jobs": "Ndodhi një gabim gjatë fshirjes së skedarëve dhe detyrave",
"go_to_downloads": "Shko te shkarkimet"
} }
} }
}, },
@@ -365,12 +350,8 @@
"video_has_finished_playing": "Videoja ka përfunduar shfaqjen!", "video_has_finished_playing": "Videoja ka përfunduar shfaqjen!",
"no_video_source": "Asnjë burim video...", "no_video_source": "Asnjë burim video...",
"next_episode": "Epizoda e ardhshme", "next_episode": "Epizoda e ardhshme",
"refresh_tracks": "Rifresko shtigjet",
"subtitle_tracks": "Shtigjet e nënteksteve:", "subtitle_tracks": "Shtigjet e nënteksteve:",
"audio_tracks": "Shtigjet audio:", "no_data_available": "Nuk ka të dhëna të disponueshme"
"playback_state": "Gjendja e rishikimit:",
"no_data_available": "Nuk ka të dhëna të disponueshme",
"index": "Indeksi:"
}, },
"item_card": { "item_card": {
"next_up": "E ardhshme", "next_up": "E ardhshme",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Fortsätt titta & nästa avsnitt", "continue_and_next_up": "Fortsätt titta & nästa avsnitt",
"recently_added_in": "Nyligen tillagt i {{libraryName}}", "recently_added_in": "Nyligen tillagt i {{libraryName}}",
"suggested_movies": "Filmförslag", "suggested_movies": "Filmförslag",
"suggested_episodes": "Föreslagna avsnitt",
"intro": { "intro": {
"welcome_to_streamyfin": "Välkommen till Streamyfin", "welcome_to_streamyfin": "Välkommen till Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "En gratis klient för Jellyfin med öppen källkod.", "a_free_and_open_source_client_for_jellyfin": "En gratis klient för Jellyfin med öppen källkod.",
@@ -261,43 +260,6 @@
"None": "Inga", "None": "Inga",
"OnlyForced": "Bara Tvingande" "OnlyForced": "Bara Tvingande"
}, },
"text_color": "Textfärg",
"background_color": "Bakgrundsfärg",
"outline_color": "Konturfärg",
"outline_thickness": "Konturtjocklek",
"background_opacity": "Bakgrundsgenomskinlighet",
"outline_opacity": "Kontursgenomskinlighet",
"bold_text": "FetStil",
"colors": {
"Black": "Svart",
"Gray": "Grå",
"Silver": "Silver",
"White": "Vit",
"Maroon": "Rödbrun",
"Red": "Röd",
"Fuchsia": "Purpur",
"Yellow": "Gul",
"Olive": "Olivgrön",
"Green": "Grön",
"Teal": "Turkos",
"Lime": "Limegrön",
"Purple": "Lila",
"Navy": "Marinblå",
"Blue": "Blå",
"Aqua": "Aqua"
},
"thickness": {
"None": "Inget",
"Thin": "Tunn",
"Normal": "Normal",
"Thick": "Tjock"
},
"subtitle_color": "Undertextfärg",
"subtitle_background_color": "Bakgrundsfärg",
"subtitle_font": "Typsnitt för undertexter",
"ksplayer_title": "KSPlayer-inställningar",
"hardware_decode": "Hårdvaruavkodning",
"hardware_decode_description": "Använd hårdvaruacceleration för videoavkodning. Inaktivera om du upplever uppspelningsproblem.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Ange din OpenSubtitles API-nyckel för att aktivera klientbaserad undertextsökning som reserv när din Jellyfin-server inte har en undertextleverantör konfigurerad.", "opensubtitles_hint": "Ange din OpenSubtitles API-nyckel för att aktivera klientbaserad undertextsökning som reserv när din Jellyfin-server inte har en undertextleverantör konfigurerad.",
"opensubtitles_api_key": "API-nyckel", "opensubtitles_api_key": "API-nyckel",
@@ -315,25 +277,6 @@
"bottom": "Botten" "bottom": "Botten"
} }
}, },
"vlc_subtitles": {
"title": "VLC undertextsinställningar",
"hint": "Anpassa undertextens utseende för VLC-spelare. Förändringar träder i kraft vid nästa uppspelning.",
"text_color": "Textfärg",
"background_color": "Bakgrundsfärg",
"background_opacity": "Bakgrundsgenomskinlighet",
"outline_color": "Konturfärg",
"outline_opacity": "Kontursgenomskinlighet",
"outline_thickness": "Konturtjocklek",
"bold": "FetStil",
"margin": "Nedre marginal"
},
"video_player": {
"title": "Videospelare",
"video_player": "Videospelare",
"video_player_description": "Välj vilken videospelare som ska användas på iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Övrigt", "other_title": "Övrigt",
"video_orientation": "Videoriktning", "video_orientation": "Videoriktning",
@@ -351,13 +294,7 @@
"UNKNOWN": "Okänt" "UNKNOWN": "Okänt"
}, },
"safe_area_in_controls": "Säkert område i kontrollerna", "safe_area_in_controls": "Säkert område i kontrollerna",
"video_player": "Videospelare",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimentell + PiP)"
},
"show_custom_menu_links": "Visa anpassade menylänkar", "show_custom_menu_links": "Visa anpassade menylänkar",
"show_large_home_carousel": "Visa toppbanner (beta)",
"hide_libraries": "Dölj bibliotek", "hide_libraries": "Dölj bibliotek",
"select_liraries_you_want_to_hide": "Välj de bibliotek du vill dölja på fliken Bibliotek och på startsidan.", "select_liraries_you_want_to_hide": "Välj de bibliotek du vill dölja på fliken Bibliotek och på startsidan.",
"disable_haptic_feedback": "Stäng av vibrationer", "disable_haptic_feedback": "Stäng av vibrationer",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Antal Avsnitt för Automatisk Uppspelning", "max_auto_play_episode_count": "Antal Avsnitt för Automatisk Uppspelning",
"disabled": "Inaktiverad" "disabled": "Inaktiverad"
}, },
"downloads": {
"downloads_title": "Nedladdningar"
},
"music": { "music": {
"title": "Musik", "title": "Musik",
"playback_title": "Uppspelning", "playback_title": "Uppspelning",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Läs mer om Marlin.", "read_more_about_marlin": "Läs mer om Marlin.",
"save_button": "Spara", "save_button": "Spara",
"toasts": { "toasts": {
"saved": "Sparade", "saved": "Sparade"
"refreshed": "Inställningarna uppdateras från servern" }
},
"refresh_from_server": "Uppdatera inställningar från server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Aktivera Streamystats",
"disable_streamystats": "Inaktivera Streamystats", "disable_streamystats": "Inaktivera Streamystats",
"enable_search": "Använd för sökning", "enable_search": "Använd för sökning",
"url": "Webbadress", "url": "Webbadress",
"server_url_placeholder": "http(s)://streamystats.exempel.se", "server_url_placeholder": "http(s)://streamystats.exempel.se",
"streamystats_search_hint": "Ange URL för Marlin-servern. URL bör innehålla http eller https och vid behov port.", "streamystats_search_hint": "Ange URL för Marlin-servern. URL bör innehålla http eller https och vid behov port.",
"read_more_about_streamystats": "Läs mer om Streamystats.", "read_more_about_streamystats": "Läs mer om Streamystats.",
"save_button": "Spara",
"save": "Spara", "save": "Spara",
"features_title": "Funktioner", "features_title": "Funktioner",
"home_sections_title": "Hemsektioner",
"enable_movie_recommendations": "Filmrekommendationer", "enable_movie_recommendations": "Filmrekommendationer",
"enable_series_recommendations": "serierekommendationer", "enable_series_recommendations": "serierekommendationer",
"enable_promoted_watchlists": "rekommenderade listor att titta på", "enable_promoted_watchlists": "rekommenderade listor att titta på",
@@ -445,8 +374,7 @@
"refresh_from_server": "Uppdatera inställningar från server" "refresh_from_server": "Uppdatera inställningar från server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Aktivera vår bevakningslista integration", "watchlist_enabler": "Aktivera vår bevakningslista integration"
"watchlist_button": "sätt på/av bevakningslisteintegrationen"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Ta bort alla nerladdade filer", "delete_all_downloaded_files": "Ta bort alla nerladdade filer",
"music_cache_title": "Musikcache", "music_cache_title": "Musikcache",
"music_cache_description": "Cacha automatiskt låtar när du lyssnar för smidigare uppspelning och offline-stöd", "music_cache_description": "Cacha automatiskt låtar när du lyssnar för smidigare uppspelning och offline-stöd",
"enable_music_cache": "Aktivera musikcache",
"clear_music_cache": "Rensa musikcache", "clear_music_cache": "Rensa musikcache",
"music_cache_size": "{{size}} cachad", "music_cache_size": "{{size}} cachad",
"music_cache_cleared": "Musikcache rensad", "music_cache_cleared": "Musikcache rensad",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Exportera Loggar", "export_logs": "Exportera Loggar",
"click_for_more_info": "Klicka för mer Information", "click_for_more_info": "Klicka för mer Information",
"level": "Nivå", "level": "Nivå",
"no_logs_available": "Inga Loggar Tillgängliga", "no_logs_available": "Inga Loggar Tillgängliga"
"delete_all_logs": "Ta Bort Alla Loggar"
}, },
"languages": { "languages": {
"title": "Språk", "title": "Språk",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Fel Vid Borttagning Av Filer", "error_deleting_files": "Fel Vid Borttagning Av Filer"
"background_downloads_enabled": "Bakgrundsnedladdningar aktiverade",
"background_downloads_disabled": "Bakgrundsnedladdningar inaktiverade"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Nedladdningar", "downloads_title": "Nedladdningar",
"tvseries": "TV-Serier", "tvseries": "TV-Serier",
"movies": "Filmer", "movies": "Filmer",
"queue": "Kö",
"other_media": "Annan media", "other_media": "Annan media",
"queue_hint": "Kö och nedladdningar kommer försvinna vid omstart av appen",
"no_items_in_queue": "Inga objekt i Kön",
"no_downloaded_items": "Inga Nedladdade Objekt", "no_downloaded_items": "Inga Nedladdade Objekt",
"delete_all_movies_button": "Ta Bort Alla Filmer", "delete_all_movies_button": "Ta Bort Alla Filmer",
"delete_all_tvseries_button": "Ta Bort Alla TV-Serier", "delete_all_tvseries_button": "Ta Bort Alla TV-Serier",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Det Gick Inte Att Ta Bort Alla TV-Serier", "failed_to_delete_all_tvseries": "Det Gick Inte Att Ta Bort Alla TV-Serier",
"deleted_media_successfully": "Andra Medier Har Tagits Bort!", "deleted_media_successfully": "Andra Medier Har Tagits Bort!",
"failed_to_delete_media": "Kunde Inte Ta Bort Andra Medier", "failed_to_delete_media": "Kunde Inte Ta Bort Andra Medier",
"download_deleted": "Nedladdning Borttagen",
"download_cancelled": "Nerladdningen Avbruten", "download_cancelled": "Nerladdningen Avbruten",
"could_not_delete_download": "Kunde Inte Ta Bort Nedladdning", "could_not_delete_download": "Kunde Inte Ta Bort Nedladdning",
"download_paused": "Nedladdning Pausad",
"could_not_pause_download": "Kunde Inte Pausa Nedladdning",
"download_resumed": "Nedladdning Återupptagen",
"could_not_resume_download": "Kunde Inte Återuppta Nedladdning",
"download_completed": "Nedladdning Slutförd", "download_completed": "Nedladdning Slutförd",
"download_failed": "Nerladdningen misslyckades", "download_failed": "Nerladdningen misslyckades",
"download_failed_for_item": "Nedladdning misslyckades för {{item}} - {{error}}", "download_failed_for_item": "Nedladdning misslyckades för {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} Laddas redan ner", "item_already_downloading": "{{item}} Laddas redan ner",
"all_files_deleted": "Alla nedladdningar raderades", "all_files_deleted": "Alla nedladdningar raderades",
"files_deleted_by_type": "{{count}} {{type}} Raderad", "files_deleted_by_type": "{{count}} {{type}} Raderad",
"all_files_folders_and_jobs_deleted_successfully": "Alla filer, mappar och jobb har tagits bort",
"failed_to_clean_cache_directory": "Det gick inte att rensa cachemappen",
"could_not_get_download_url_for_item": "Kunde inte hämta nedladdnings-URL för {{itemName}}", "could_not_get_download_url_for_item": "Kunde inte hämta nedladdnings-URL för {{itemName}}",
"go_to_downloads": "Gå till nedladdningar",
"file_deleted": "{{item}} Raderad" "file_deleted": "{{item}} Raderad"
} }
} }
@@ -583,16 +493,13 @@
"none": "Ingen", "none": "Ingen",
"track": "Spår", "track": "Spår",
"cancel": "Avbryt", "cancel": "Avbryt",
"stop": "Stoppa",
"delete": "Ta bort", "delete": "Ta bort",
"ok": "OK", "ok": "OK",
"remove": "Radera", "remove": "Radera",
"next": "Nästa",
"back": "Tillbaka", "back": "Tillbaka",
"continue": "Fortsätt", "continue": "Fortsätt",
"verifying": "Verifierar...", "verifying": "Verifierar...",
"login": "Logga in", "login": "Logga in"
"refresh": "Uppdatera"
}, },
"search": { "search": {
"search": "Sök...", "search": "Sök...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast", "could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast",
"message_from_server": "Meddelande från servern: {{message}}", "message_from_server": "Meddelande från servern: {{message}}",
"next_episode": "Nästa avsnitt", "next_episode": "Nästa avsnitt",
"refresh_tracks": "Uppdatera spår",
"audio_tracks": "Ljudspår:",
"playback_state": "Uppspelningsstatus:",
"index": "Index:",
"continue_watching": "Fortsätt titta", "continue_watching": "Fortsätt titta",
"go_back": "Tillbaka", "go_back": "Tillbaka",
"downloaded_file_title": "Du har denna fil nedladdad", "downloaded_file_title": "Du har denna fil nedladdad",
@@ -761,7 +664,6 @@
"show_more": "Visa Mer", "show_more": "Visa Mer",
"show_less": "Visa Mindre", "show_less": "Visa Mindre",
"left": "kvar", "left": "kvar",
"more_info": "Mer info",
"director": "Regissör", "director": "Regissör",
"cast": "Skådespelare", "cast": "Skådespelare",
"technical_details": "Tekniska detaljer", "technical_details": "Tekniska detaljer",
@@ -888,13 +790,9 @@
"playlists": "Spellistor", "playlists": "Spellistor",
"tracks": "spår" "tracks": "spår"
}, },
"filters": {
"all": "Alla"
},
"recently_added": "Nyligen tillagt", "recently_added": "Nyligen tillagt",
"recently_played": "Nyligen spelat", "recently_played": "Nyligen spelat",
"frequently_played": "Spelas ofta", "frequently_played": "Spelas ofta",
"explore": "Utforska",
"top_tracks": "Toppspår", "top_tracks": "Toppspår",
"play": "Spela", "play": "Spela",
"shuffle": "Blanda spår", "shuffle": "Blanda spår",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Recently Added in {{libraryName}}", "recently_added_in": "Recently Added in {{libraryName}}",
"suggested_movies": "Suggested Movies", "suggested_movies": "Suggested Movies",
"suggested_episodes": "Suggested Episodes",
"intro": { "intro": {
"welcome_to_streamyfin": "Welcome to Streamyfin", "welcome_to_streamyfin": "Welcome to Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
@@ -261,43 +260,6 @@
"None": "None", "None": "None",
"OnlyForced": "OnlyForced" "OnlyForced": "OnlyForced"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "สีน้ำเงิน",
"Aqua": "Aqua"
},
"thickness": {
"None": "None",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Other", "other_title": "Other",
"video_orientation": "Video Orientation", "video_orientation": "Video Orientation",
@@ -351,13 +294,7 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "Safe Area in Controls", "safe_area_in_controls": "Safe Area in Controls",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Show Custom Menu Links", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries", "hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback", "disable_haptic_feedback": "Disable Haptic Feedback",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Read More About Marlin.", "read_more_about_marlin": "Read More About Marlin.",
"save_button": "Save", "save_button": "Save",
"toasts": { "toasts": {
"saved": "Saved", "saved": "Saved"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Delete All Downloaded Files", "delete_all_downloaded_files": "Delete All Downloaded Files",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export Logs", "export_logs": "Export Logs",
"click_for_more_info": "Click for More Info", "click_for_more_info": "Click for More Info",
"level": "Level", "level": "Level",
"no_logs_available": "No Logs Available", "no_logs_available": "No Logs Available"
"delete_all_logs": "Delete All Logs"
}, },
"languages": { "languages": {
"title": "Languages", "title": "Languages",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-Series", "tvseries": "TV-Series",
"movies": "Movies", "movies": "Movies",
"queue": "Queue",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Queue and downloads will be lost on app restart",
"no_items_in_queue": "No Items in Queue",
"no_downloaded_items": "No Downloaded Items", "no_downloaded_items": "No Downloaded Items",
"delete_all_movies_button": "Delete All Movies", "delete_all_movies_button": "Delete All Movies",
"delete_all_tvseries_button": "Delete All TV-Series", "delete_all_tvseries_button": "Delete All TV-Series",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Download Completed", "download_completed": "Download Completed",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}", "download_failed_for_item": "Download failed for {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode", "next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching", "continue_watching": "Continue Watching",
"go_back": "Go Back", "go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "num tu'lu' {{libraryName}}", "recently_added_in": "num tu'lu' {{libraryName}}",
"suggested_movies": "rutlh DIS", "suggested_movies": "rutlh DIS",
"suggested_episodes": "rutlh Hem",
"intro": { "intro": {
"welcome_to_streamyfin": "Streamyfin yI'el!", "welcome_to_streamyfin": "Streamyfin yI'el!",
"a_free_and_open_source_client_for_jellyfin": "Jellyfin lut 'el je'be' 'ej wang.", "a_free_and_open_source_client_for_jellyfin": "Jellyfin lut 'el je'be' 'ej wang.",
@@ -261,43 +260,6 @@
"None": "pagh", "None": "pagh",
"OnlyForced": "Dun je'" "OnlyForced": "Dun je'"
}, },
"text_color": "GhItlh rIt",
"background_color": "Tlhagh rIt",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "pagh",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "patlh", "other_title": "patlh",
"video_orientation": "mu'tlhegh pegh", "video_orientation": "mu'tlhegh pegh",
@@ -351,13 +294,7 @@
"UNKNOWN": "Sovbe'" "UNKNOWN": "Sovbe'"
}, },
"safe_area_in_controls": "SeHlawDaq yot QIH", "safe_area_in_controls": "SeHlawDaq yot QIH",
"video_player": "mu'tlhegh tlholwI'",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (PiP mIwHa')"
},
"show_custom_menu_links": "menuDaq ret teqlu' yInej", "show_custom_menu_links": "menuDaq ret teqlu' yInej",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "De'wI' bom yIQIj", "hide_libraries": "De'wI' bom yIQIj",
"select_liraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.", "select_liraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.",
"disable_haptic_feedback": "Qub quvHa' yIQIj", "disable_haptic_feedback": "Qub quvHa' yIQIj",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Qaw' Doch"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Marlin latlh yIlaD", "read_more_about_marlin": "Marlin latlh yIlaD",
"save_button": "yIqIp", "save_button": "yIqIp",
"toasts": { "toasts": {
"saved": "qIp", "saved": "qIp"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'", "delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "De' qon yISamqa'", "export_logs": "De' qon yISamqa'",
"click_for_more_info": "latlh De' yIchIch", "click_for_more_info": "latlh De' yIchIch",
"level": "quv", "level": "quv",
"no_logs_available": "De' qon pagh", "no_logs_available": "De' qon pagh"
"delete_all_logs": "Hoch De' qon yIQaw'"
}, },
"languages": { "languages": {
"title": "Holmey", "title": "Holmey",
@@ -490,15 +414,12 @@
"system": "mIw'a'" "system": "mIw'a'"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Qaw' ghIq", "error_deleting_files": "Qaw' ghIq"
"background_downloads_enabled": "tlhegh Qaw' chu'",
"background_downloads_disabled": "tlhegh Qaw' QIj"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Qaw' Doch", "downloads_title": "Qaw' Doch",
"tvseries": "TV Hem", "tvseries": "TV Hem",
"movies": "DIS", "movies": "DIS",
"queue": "ghom",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "ghun ghImDI' ghom Qaw'laH.",
"no_items_in_queue": "ghom Doch pagh",
"no_downloaded_items": "Qaw' Doch pagh", "no_downloaded_items": "Qaw' Doch pagh",
"delete_all_movies_button": "Hoch DIS yIQaw'", "delete_all_movies_button": "Hoch DIS yIQaw'",
"delete_all_tvseries_button": "Hoch TV Hem yIQaw'", "delete_all_tvseries_button": "Hoch TV Hem yIQaw'",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Hoch TV Hem Qaw'laHbe'", "failed_to_delete_all_tvseries": "Hoch TV Hem Qaw'laHbe'",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Qaw' ghIm", "download_cancelled": "Qaw' ghIm",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Qaw' Qapla'", "download_completed": "Qaw' Qapla'",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "{{item}} Qaw'laHbe' - {{error}}", "download_failed_for_item": "{{item}} Qaw'laHbe' - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Hoch De', ram 'ej vum Qaw' Qapla'",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Qaw' Doch yIghoS",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "yISam...", "search": "yISam...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'", "could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'",
"message_from_server": "Ho'Do' veS jach: {{message}}", "message_from_server": "Ho'Do' veS jach: {{message}}",
"next_episode": "wej HemHom", "next_episode": "wej HemHom",
"refresh_tracks": "ret yIchu'qa'",
"audio_tracks": "QoQ ret:",
"playback_state": "tlhol mIw:",
"index": "nem:",
"continue_watching": "tlhol yIHaDqa'", "continue_watching": "tlhol yIHaDqa'",
"go_back": "Go Back", "go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "latlh yIHoch", "show_more": "latlh yIHoch",
"show_less": "Hom yIHoch", "show_less": "Hom yIHoch",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "İzlemeye Devam Et & Sıradakiler", "continue_and_next_up": "İzlemeye Devam Et & Sıradakiler",
"recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler", "recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler",
"suggested_movies": "Önerilen Filmler", "suggested_movies": "Önerilen Filmler",
"suggested_episodes": "Önerilen Bölümler",
"intro": { "intro": {
"welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz",
"a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.",
@@ -261,43 +260,6 @@
"None": "Yok", "None": "Yok",
"OnlyForced": "Sadece Zorunlu" "OnlyForced": "Sadece Zorunlu"
}, },
"text_color": "Metin Rengi",
"background_color": "Arkaplan Rengi",
"outline_color": "Kenarlık Rengi",
"outline_thickness": "Kenarlık kalınlığı",
"background_opacity": "Arkaplan Opaklığı",
"outline_opacity": "Kenarlık Opaklığı",
"bold_text": "Kalın Metin",
"colors": {
"Black": "Siyah",
"Gray": "Gri",
"Silver": "Gümüş",
"White": "Beyaz",
"Maroon": "Kestane",
"Red": "Kırmızı",
"Fuchsia": "Fuşya",
"Yellow": "Sarı",
"Olive": "Zeytin yeşili",
"Green": "Yeşil",
"Teal": "Deniz mavisi",
"Lime": "Limon",
"Purple": "Mor",
"Navy": "Lacivert",
"Blue": "Mavi",
"Aqua": "Açık Mavi"
},
"thickness": {
"None": "Hiçbiri",
"Thin": "İnce",
"Normal": "Normal",
"Thick": "Kalın"
},
"subtitle_color": "Altyazı Rengi",
"subtitle_background_color": "Arkaplan Rengi",
"subtitle_font": "Altyazı Yazı Tipi",
"ksplayer_title": "KSPlayer Ayarları",
"hardware_decode": "Donanımsal Kod Çözme",
"hardware_decode_description": "Video kod çözme için donanımsal hızlandırma kullan. Oynatma sorunları yaşıyorsanız devre dışı bırakın.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Altyazı Ayarları",
"hint": "VLC oynatıcı için altyazı görünümünü değiştirin. Değişiklikler bir sonraki oynatmada etkili olacak.",
"text_color": "Metin Rengi",
"background_color": "Arkaplan Rengi",
"background_opacity": "Arkaplan Opaklığı",
"outline_color": "Kenarlık Rengi",
"outline_opacity": "Kenarlık Opaklığı",
"outline_thickness": "Kenarlık Kalınlığı",
"bold": "Kalın Metin",
"margin": "Alt Kenar Boşluğu"
},
"video_player": {
"title": "Video oynatıcısı",
"video_player": "Video oynatıcısı",
"video_player_description": "iOS'da hangi video oynatıcının kullanılacağını seçin.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Diğer", "other_title": "Diğer",
"video_orientation": "Video Yönü", "video_orientation": "Video Yönü",
@@ -351,13 +294,7 @@
"UNKNOWN": "Bilinmeyen" "UNKNOWN": "Bilinmeyen"
}, },
"safe_area_in_controls": "Kontrollerde Güvenli Alan", "safe_area_in_controls": "Kontrollerde Güvenli Alan",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Deneysel + PiP)"
},
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster", "show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Kütüphaneleri Gizle", "hide_libraries": "Kütüphaneleri Gizle",
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak", "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "En Fazla Otomatik Oynatılacak Bölüm Sayısı", "max_auto_play_episode_count": "En Fazla Otomatik Oynatılacak Bölüm Sayısı",
"disabled": "Devre dışı" "disabled": "Devre dışı"
}, },
"downloads": {
"downloads_title": "İndirmeler"
},
"music": { "music": {
"title": "Müzik", "title": "Müzik",
"playback_title": "Oynatma", "playback_title": "Oynatma",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Marlin hakkında daha fazla oku.", "read_more_about_marlin": "Marlin hakkında daha fazla oku.",
"save_button": "Kaydet", "save_button": "Kaydet",
"toasts": { "toasts": {
"saved": "Kaydedildi", "saved": "Kaydedildi"
"refreshed": "Ayarlar sunucudan yeniden alındı" }
},
"refresh_from_server": "Ayarları Sunucudan Yeniden Al"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Streamystats'ı Etkinleştir",
"disable_streamystats": "Streamystats'ı Devre Dışı Bırak", "disable_streamystats": "Streamystats'ı Devre Dışı Bırak",
"enable_search": "Arama için kullan", "enable_search": "Arama için kullan",
"url": "URL Adresi", "url": "URL Adresi",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Streamystats sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", "streamystats_search_hint": "Streamystats sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.",
"read_more_about_streamystats": "Streamystats hakkında daha fazla bilgi.", "read_more_about_streamystats": "Streamystats hakkında daha fazla bilgi.",
"save_button": "Kaydet",
"save": "Kaydet", "save": "Kaydet",
"features_title": "Özellikler", "features_title": "Özellikler",
"home_sections_title": "Home Sections",
"enable_movie_recommendations": "Film Önerileri", "enable_movie_recommendations": "Film Önerileri",
"enable_series_recommendations": "Dizi Önerileri", "enable_series_recommendations": "Dizi Önerileri",
"enable_promoted_watchlists": "Promoted Watchlists", "enable_promoted_watchlists": "Promoted Watchlists",
@@ -445,8 +374,7 @@
"refresh_from_server": "Ayarları Sunucudan Yeniden Al" "refresh_from_server": "Ayarları Sunucudan Yeniden Al"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Tüm indirilen dosyaları sil", "delete_all_downloaded_files": "Tüm indirilen dosyaları sil",
"music_cache_title": "Müzik Ön Belleği", "music_cache_title": "Müzik Ön Belleği",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Müzik Ön Belleğini Etkinleştir",
"clear_music_cache": "Müzik Ön Belleğini Temizle", "clear_music_cache": "Müzik Ön Belleğini Temizle",
"music_cache_size": "{{size}} ön belleklendi", "music_cache_size": "{{size}} ön belleklendi",
"music_cache_cleared": "Müzik ön belleği temizlendi", "music_cache_cleared": "Müzik ön belleği temizlendi",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Düzey", "level": "Düzey",
"no_logs_available": "Günlükler mevcut değil", "no_logs_available": "Günlükler mevcut değil"
"delete_all_logs": "Tüm günlükleri sil"
}, },
"languages": { "languages": {
"title": "Diller", "title": "Diller",
@@ -490,15 +414,12 @@
"system": "Sistem" "system": "Sistem"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Dosyalar silinirken hata oluştu", "error_deleting_files": "Dosyalar silinirken hata oluştu"
"background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi",
"background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "İndirilenler", "downloads_title": "İndirilenler",
"tvseries": "Diziler", "tvseries": "Diziler",
"movies": "Filmler", "movies": "Filmler",
"queue": "Sıra",
"other_media": "Diğer medya", "other_media": "Diğer medya",
"queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır",
"no_items_in_queue": "Sırada öğe yok",
"no_downloaded_items": "İndirilen öğe yok", "no_downloaded_items": "İndirilen öğe yok",
"delete_all_movies_button": "Tüm Filmleri Sil", "delete_all_movies_button": "Tüm Filmleri Sil",
"delete_all_tvseries_button": "Tüm Dizileri Sil", "delete_all_tvseries_button": "Tüm Dizileri Sil",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Diziler silinemedi", "failed_to_delete_all_tvseries": "Diziler silinemedi",
"deleted_media_successfully": "Diğer medya başarıyla silindi!", "deleted_media_successfully": "Diğer medya başarıyla silindi!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "İndirme silindi",
"download_cancelled": "İndirme iptal edildi", "download_cancelled": "İndirme iptal edildi",
"could_not_delete_download": "İndirme Silinemedi", "could_not_delete_download": "İndirme Silinemedi",
"download_paused": "İndirme Duraklatıldı",
"could_not_pause_download": "İndirme Duraklatılamadı",
"download_resumed": "İndirme Devam Ediyor",
"could_not_resume_download": "İndirme Devam Ettirilemedi",
"download_completed": "İndirme tamamlandı", "download_completed": "İndirme tamamlandı",
"download_failed": "İndirme başarısız oldu", "download_failed": "İndirme başarısız oldu",
"download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} zaten indiriliyor", "item_already_downloading": "{{item}} zaten indiriliyor",
"all_files_deleted": "Bütün indirilenler başarıyla silindi", "all_files_deleted": "Bütün indirilenler başarıyla silindi",
"files_deleted_by_type": "{{count}} {{type}} silindi", "files_deleted_by_type": "{{count}} {{type}} silindi",
"all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi",
"failed_to_clean_cache_directory": "Önbellek dizini temizlenemedi",
"could_not_get_download_url_for_item": "{{itemName}} için indirme URL'si alınamadı", "could_not_get_download_url_for_item": "{{itemName}} için indirme URL'si alınamadı",
"go_to_downloads": "İndirmelere git",
"file_deleted": "{{item}} silindi" "file_deleted": "{{item}} silindi"
} }
} }
@@ -583,16 +493,13 @@
"none": "Hiçbiri", "none": "Hiçbiri",
"track": "Parça", "track": "Parça",
"cancel": "Vazgeç", "cancel": "Vazgeç",
"stop": "Stop",
"delete": "Sil", "delete": "Sil",
"ok": "Tamam", "ok": "Tamam",
"remove": "Kaldır", "remove": "Kaldır",
"next": "Sonraki",
"back": "Geri", "back": "Geri",
"continue": "Devam", "continue": "Devam",
"verifying": "Doğrulanıyor...", "verifying": "Doğrulanıyor...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Ara...", "search": "Ara...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı",
"message_from_server": "Sunucudan mesaj: {{message}}", "message_from_server": "Sunucudan mesaj: {{message}}",
"next_episode": "Sonraki bölüm", "next_episode": "Sonraki bölüm",
"refresh_tracks": "Parçaları yenile",
"audio_tracks": "Ses Parçaları:",
"playback_state": "Oynatma Durumu:",
"index": "İndeks:",
"continue_watching": "İzlemeye devam et", "continue_watching": "İzlemeye devam et",
"go_back": "Geri", "go_back": "Geri",
"downloaded_file_title": "Bu dosya indirilmiş", "downloaded_file_title": "Bu dosya indirilmiş",
@@ -761,7 +664,6 @@
"show_more": "Daha fazla göster", "show_more": "Daha fazla göster",
"show_less": "Daha az göster", "show_less": "Daha az göster",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Çalma listeleri", "playlists": "Çalma listeleri",
"tracks": "parçalar" "tracks": "parçalar"
}, },
"filters": {
"all": "Tümü"
},
"recently_added": "Son Eklenenler", "recently_added": "Son Eklenenler",
"recently_played": "Son Oynatılanlar", "recently_played": "Son Oynatılanlar",
"frequently_played": "Sık Oynatılanlar", "frequently_played": "Sık Oynatılanlar",
"explore": "Keşfet",
"top_tracks": "En Popülar Parçalar", "top_tracks": "En Popülar Parçalar",
"play": "Oynat", "play": "Oynat",
"shuffle": "Karıştır", "shuffle": "Karıştır",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Нещодавно додане до \"{{libraryName}}\"", "recently_added_in": "Нещодавно додане до \"{{libraryName}}\"",
"suggested_movies": "Рекомендовані Фільми", "suggested_movies": "Рекомендовані Фільми",
"suggested_episodes": "Рекомендовані Епізоди",
"intro": { "intro": {
"welcome_to_streamyfin": "Вітаємо у Streamyfin", "welcome_to_streamyfin": "Вітаємо у Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Вільний і open-source клієнт для Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Вільний і open-source клієнт для Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Някий", "None": "Някий",
"OnlyForced": "Виключно Форсовані" "OnlyForced": "Виключно Форсовані"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "None",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Інші", "other_title": "Інші",
"video_orientation": "Орієнтація відео", "video_orientation": "Орієнтація відео",
@@ -351,13 +294,7 @@
"UNKNOWN": "Невідомо" "UNKNOWN": "Невідомо"
}, },
"safe_area_in_controls": "Безпечна зона в елементах керування", "safe_area_in_controls": "Безпечна зона в елементах керування",
"video_player": "Відео плеєр",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Показати користувацькі посилання меню", "show_custom_menu_links": "Показати користувацькі посилання меню",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Сховати медіатеки", "hide_libraries": "Сховати медіатеки",
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.", "select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок", "disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Вимкнено" "disabled": "Вимкнено"
}, },
"downloads": {
"downloads_title": "Завантаження"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Дізнайтеся більше про Marlin.", "read_more_about_marlin": "Дізнайтеся більше про Marlin.",
"save_button": "Зберегти", "save_button": "Зберегти",
"toasts": { "toasts": {
"saved": "Збережено", "saved": "Збережено"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Видалити усі завантаженні файли", "delete_all_downloaded_files": "Видалити усі завантаженні файли",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Level", "level": "Level",
"no_logs_available": "Нема доступних журналів", "no_logs_available": "Нема доступних журналів"
"delete_all_logs": "Видалити усі журнали"
}, },
"languages": { "languages": {
"title": "Мова", "title": "Мова",
@@ -490,15 +414,12 @@
"system": "Системна" "system": "Системна"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Помилка при видалені файлів", "error_deleting_files": "Помилка при видалені файлів"
"background_downloads_enabled": "Завантаження в фоні увімкнене",
"background_downloads_disabled": "Завантаження в фоні вимкнене"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Завантаження", "downloads_title": "Завантаження",
"tvseries": "ТБ-Серіали", "tvseries": "ТБ-Серіали",
"movies": "Фільми", "movies": "Фільми",
"queue": "Черга",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Черга і завантаження буде втрачене при перезапуску застосунку",
"no_items_in_queue": "Нема елементів в черзі",
"no_downloaded_items": "Нема завантажених елементів", "no_downloaded_items": "Нема завантажених елементів",
"delete_all_movies_button": "Видалити всі Фільми", "delete_all_movies_button": "Видалити всі Фільми",
"delete_all_tvseries_button": "Видалити всі ТБ-Серіали", "delete_all_tvseries_button": "Видалити всі ТБ-Серіали",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали", "failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Завантаження скасоване", "download_cancelled": "Завантаження скасоване",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Завантаження завершено", "download_completed": "Завантаження завершено",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Не вдалося завантажити {{item}} - {{error}}", "download_failed_for_item": "Не вдалося завантажити {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Усі файли, папки та завдання успішно видалено",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Перейти до завантаження",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Шукати...", "search": "Шукати...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast", "could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
"message_from_server": "Повідомлення від серверу: {{message}}", "message_from_server": "Повідомлення від серверу: {{message}}",
"next_episode": "Наступний Епізод", "next_episode": "Наступний Епізод",
"refresh_tracks": "Оновити доріжки",
"audio_tracks": "Аудіо-доріжки:",
"playback_state": "Стан відтворення:",
"index": "Індекс:",
"continue_watching": "Продовжити перегляд", "continue_watching": "Продовжити перегляд",
"go_back": "Назад", "go_back": "Назад",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Показати більше", "show_more": "Показати більше",
"show_less": "Показати менше", "show_less": "Показати менше",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Mới thêm trong {{libraryName}}", "recently_added_in": "Mới thêm trong {{libraryName}}",
"suggested_movies": "Phim gợi ý", "suggested_movies": "Phim gợi ý",
"suggested_episodes": "Tập gợi ý",
"intro": { "intro": {
"welcome_to_streamyfin": "Chào mừng đến với Streamyfin", "welcome_to_streamyfin": "Chào mừng đến với Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Một ứng dụng miễn phí và mã nguồn mở cho Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "Một ứng dụng miễn phí và mã nguồn mở cho Jellyfin.",
@@ -261,43 +260,6 @@
"None": "Không hiển thị", "None": "Không hiển thị",
"OnlyForced": "Bắt buộc" "OnlyForced": "Bắt buộc"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "Không có",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Khác", "other_title": "Khác",
"video_orientation": "Hướng video", "video_orientation": "Hướng video",
@@ -351,13 +294,7 @@
"UNKNOWN": "Không rõ" "UNKNOWN": "Không rõ"
}, },
"safe_area_in_controls": "Vùng an toàn trong điều khiển", "safe_area_in_controls": "Vùng an toàn trong điều khiển",
"video_player": "Trình phát video",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Thử nghiệm + PiP)"
},
"show_custom_menu_links": "Hiện liên kết tùy chỉnh", "show_custom_menu_links": "Hiện liên kết tùy chỉnh",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Ẩn thư viện", "hide_libraries": "Ẩn thư viện",
"select_liraries_you_want_to_hide": "Chọn các thư viện muốn ẩn khỏi mục Thư viện và Trang chủ.", "select_liraries_you_want_to_hide": "Chọn các thư viện muốn ẩn khỏi mục Thư viện và Trang chủ.",
"disable_haptic_feedback": "Tắt phản hồi rung", "disable_haptic_feedback": "Tắt phản hồi rung",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Số tập tự chạy tối đa", "max_auto_play_episode_count": "Số tập tự chạy tối đa",
"disabled": "Đã tắt" "disabled": "Đã tắt"
}, },
"downloads": {
"downloads_title": "Tải xuống"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Tìm hiểu thêm về Marlin.", "read_more_about_marlin": "Tìm hiểu thêm về Marlin.",
"save_button": "Lưu", "save_button": "Lưu",
"toasts": { "toasts": {
"saved": "Đã lưu", "saved": "Đã lưu"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Xóa toàn bộ tập tin đã tải", "delete_all_downloaded_files": "Xóa toàn bộ tập tin đã tải",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Xuất nhật ký", "export_logs": "Xuất nhật ký",
"click_for_more_info": "Nhấn để xem thêm thông tin", "click_for_more_info": "Nhấn để xem thêm thông tin",
"level": "Mức độ", "level": "Mức độ",
"no_logs_available": "Không có nhật ký", "no_logs_available": "Không có nhật ký"
"delete_all_logs": "Xóa tất cả nhật ký"
}, },
"languages": { "languages": {
"title": "Ngôn ngữ", "title": "Ngôn ngữ",
@@ -490,15 +414,12 @@
"system": "Hệ thống" "system": "Hệ thống"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Lỗi khi xóa tập tin", "error_deleting_files": "Lỗi khi xóa tập tin"
"background_downloads_enabled": "Tải trong nền đã bật",
"background_downloads_disabled": "Tải trong nền đã tắt"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Tải xuống", "downloads_title": "Tải xuống",
"tvseries": "Chương trình TV", "tvseries": "Chương trình TV",
"movies": "Phim", "movies": "Phim",
"queue": "Hàng đợi",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Hàng đợi và tải xuống sẽ bị mất khi khởi động lại ứng dụng",
"no_items_in_queue": "Không có mục trong hàng đợi",
"no_downloaded_items": "Không có mục đã tải", "no_downloaded_items": "Không có mục đã tải",
"delete_all_movies_button": "Xóa tất cả phim", "delete_all_movies_button": "Xóa tất cả phim",
"delete_all_tvseries_button": "Xóa tất cả chương trình TV", "delete_all_tvseries_button": "Xóa tất cả chương trình TV",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Xóa chương trình TV thất bại", "failed_to_delete_all_tvseries": "Xóa chương trình TV thất bại",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Tải xuống bị hủy", "download_cancelled": "Tải xuống bị hủy",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Tải xuống hoàn tất", "download_completed": "Tải xuống hoàn tất",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Tải {{item}} thất bại {{error}}", "download_failed_for_item": "Tải {{item}} thất bại {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Đã xóa thành công tất cả tập tin, thư mục và công việc",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Tới phần tải về",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Tìm...", "search": "Tìm...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Không thể tạo luồng cho Chromecast", "could_not_create_stream_for_chromecast": "Không thể tạo luồng cho Chromecast",
"message_from_server": "Thông báo từ máy chủ: {{message}}", "message_from_server": "Thông báo từ máy chủ: {{message}}",
"next_episode": "Tập tiếp theo", "next_episode": "Tập tiếp theo",
"refresh_tracks": "Làm mới các track",
"audio_tracks": "Track âm thanh:",
"playback_state": "Trạng thái phát:",
"index": "Chỉ mục:",
"continue_watching": "Tiếp tục xem", "continue_watching": "Tiếp tục xem",
"go_back": "Quay lại", "go_back": "Quay lại",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Xem thêm", "show_more": "Xem thêm",
"show_less": "Thu gọn", "show_less": "Thu gọn",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -43,7 +43,6 @@
"next_up": "下一个", "next_up": "下一个",
"recently_added_in": "最近添加于 {{libraryName}}", "recently_added_in": "最近添加于 {{libraryName}}",
"suggested_movies": "推荐电影", "suggested_movies": "推荐电影",
"suggested_episodes": "推荐剧集",
"intro": { "intro": {
"welcome_to_streamyfin": "欢迎来到 Streamyfin", "welcome_to_streamyfin": "欢迎来到 Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。",
@@ -128,18 +127,12 @@
"UNKNOWN": "未知" "UNKNOWN": "未知"
}, },
"safe_area_in_controls": "控制中的安全区域", "safe_area_in_controls": "控制中的安全区域",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "显示自定义菜单链接", "show_custom_menu_links": "显示自定义菜单链接",
"hide_libraries": "隐藏媒体库", "hide_libraries": "隐藏媒体库",
"select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。",
"disable_haptic_feedback": "禁用触觉反馈" "disable_haptic_feedback": "禁用触觉反馈"
}, },
"downloads": { "downloads": {
"downloads_title": "下载",
"optimized_versions_server": "Optimized Version 服务器", "optimized_versions_server": "Optimized Version 服务器",
"save_button": "保存", "save_button": "保存",
"optimized_server": "Optimized Server", "optimized_server": "Optimized Server",
@@ -204,8 +197,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Level", "level": "Level",
"no_logs_available": "无可用日志", "no_logs_available": "无可用日志"
"delete_all_logs": "删除所有日志"
}, },
"languages": { "languages": {
"title": "语言", "title": "语言",
@@ -215,8 +207,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "删除文件时出错", "error_deleting_files": "删除文件时出错",
"background_downloads_enabled": "后台下载已启用",
"background_downloads_disabled": "后台下载已禁用",
"connected": "已连接", "connected": "已连接",
"could_not_connect": "无法连接", "could_not_connect": "无法连接",
"invalid_url": "无效 URL" "invalid_url": "无效 URL"
@@ -226,9 +216,6 @@
"downloads_title": "下载", "downloads_title": "下载",
"tvseries": "剧集", "tvseries": "剧集",
"movies": "电影", "movies": "电影",
"queue": "队列",
"queue_hint": "应用重启后队列和下载将会丢失",
"no_items_in_queue": "队列中无项目",
"no_downloaded_items": "无已下载项目", "no_downloaded_items": "无已下载项目",
"delete_all_movies_button": "删除所有电影", "delete_all_movies_button": "删除所有电影",
"delete_all_tvseries_button": "删除所有剧集", "delete_all_tvseries_button": "删除所有剧集",
@@ -264,9 +251,7 @@
"no_response_received_from_server": "未收到服务器响应", "no_response_received_from_server": "未收到服务器响应",
"error_setting_up_the_request": "设置请求时出错", "error_setting_up_the_request": "设置请求时出错",
"failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误",
"all_files_folders_and_jobs_deleted_successfully": "所有文件、文件和任务成功删除", "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误"
"an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误",
"go_to_downloads": "前往下载"
} }
} }
}, },
@@ -360,12 +345,8 @@
"video_has_finished_playing": "视频播放完成!", "video_has_finished_playing": "视频播放完成!",
"no_video_source": "无视频来源...", "no_video_source": "无视频来源...",
"next_episode": "下一集", "next_episode": "下一集",
"refresh_tracks": "刷新轨道",
"subtitle_tracks": "字幕轨道:", "subtitle_tracks": "字幕轨道:",
"audio_tracks": "音频轨道:",
"playback_state": "播放状态:",
"no_data_available": "无可用数据", "no_data_available": "无可用数据",
"index": "索引:",
"continue_watching": "继续观看", "continue_watching": "继续观看",
"go_back": "返回" "go_back": "返回"
}, },

View File

@@ -43,7 +43,6 @@
"next_up": "下一個", "next_up": "下一個",
"recently_added_in": "最近添加於 {{libraryName}}", "recently_added_in": "最近添加於 {{libraryName}}",
"suggested_movies": "推薦電影", "suggested_movies": "推薦電影",
"suggested_episodes": "推薦劇集",
"intro": { "intro": {
"welcome_to_streamyfin": "歡迎來到 Streamyfin", "welcome_to_streamyfin": "歡迎來到 Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。",
@@ -128,11 +127,6 @@
"UNKNOWN": "未知" "UNKNOWN": "未知"
}, },
"safe_area_in_controls": "控制中的安全區域", "safe_area_in_controls": "控制中的安全區域",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "顯示自定義菜單鏈接", "show_custom_menu_links": "顯示自定義菜單鏈接",
"hide_libraries": "隱藏媒體庫", "hide_libraries": "隱藏媒體庫",
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
@@ -142,7 +136,6 @@
"disabled": "已停用" "disabled": "已停用"
}, },
"downloads": { "downloads": {
"downloads_title": "下載",
"optimized_versions_server": "Optimized Version 伺服器", "optimized_versions_server": "Optimized Version 伺服器",
"save_button": "保存", "save_button": "保存",
"optimized_server": "Optimized Server", "optimized_server": "Optimized Server",
@@ -207,8 +200,7 @@
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Level", "level": "Level",
"no_logs_available": "無可用日誌", "no_logs_available": "無可用日誌"
"delete_all_logs": "刪除所有日誌"
}, },
"languages": { "languages": {
"title": "語言", "title": "語言",
@@ -218,8 +210,6 @@
}, },
"toasts": { "toasts": {
"error_deleting_files": "刪除文件時出錯", "error_deleting_files": "刪除文件時出錯",
"background_downloads_enabled": "背景下載已啟用",
"background_downloads_disabled": "背景下載已禁用",
"connected": "已連接", "connected": "已連接",
"could_not_connect": "無法連接", "could_not_connect": "無法連接",
"invalid_url": "無效的 URL" "invalid_url": "無效的 URL"
@@ -233,9 +223,6 @@
"downloads_title": "下載", "downloads_title": "下載",
"tvseries": "電視劇", "tvseries": "電視劇",
"movies": "電影", "movies": "電影",
"queue": "隊列",
"queue_hint": "應用重啟後隊列和下載將會丟失",
"no_items_in_queue": "隊列中無項目",
"no_downloaded_items": "無已下載項目", "no_downloaded_items": "無已下載項目",
"delete_all_movies_button": "刪除所有電影", "delete_all_movies_button": "刪除所有電影",
"delete_all_tvseries_button": "刪除所有電視劇", "delete_all_tvseries_button": "刪除所有電視劇",
@@ -271,9 +258,7 @@
"no_response_received_from_server": "未收到伺服器的響應", "no_response_received_from_server": "未收到伺服器的響應",
"error_setting_up_the_request": "設置請求時出錯", "error_setting_up_the_request": "設置請求時出錯",
"failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤",
"all_files_folders_and_jobs_deleted_successfully": "所有文件、文件和任務成功刪除", "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤"
"an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤",
"go_to_downloads": "前往下載"
} }
} }
}, },
@@ -367,12 +352,8 @@
"video_has_finished_playing": "影片播放完畢!", "video_has_finished_playing": "影片播放完畢!",
"no_video_source": "無影片來源...", "no_video_source": "無影片來源...",
"next_episode": "下一集", "next_episode": "下一集",
"refresh_tracks": "刷新軌道",
"subtitle_tracks": "字幕軌道:", "subtitle_tracks": "字幕軌道:",
"audio_tracks": "音頻軌道:",
"playback_state": "播放狀態:",
"no_data_available": "無可用數據", "no_data_available": "無可用數據",
"index": "索引:",
"continue_watching": "繼續觀看", "continue_watching": "繼續觀看",
"go_back": "返回" "go_back": "返回"
}, },

View File

@@ -100,7 +100,6 @@
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Recently Added in {{libraryName}}", "recently_added_in": "Recently Added in {{libraryName}}",
"suggested_movies": "Suggested Movies", "suggested_movies": "Suggested Movies",
"suggested_episodes": "Suggested Episodes",
"intro": { "intro": {
"welcome_to_streamyfin": "Welcome to Streamyfin", "welcome_to_streamyfin": "Welcome to Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.", "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
@@ -261,43 +260,6 @@
"None": "None", "None": "None",
"OnlyForced": "OnlyForced" "OnlyForced": "OnlyForced"
}, },
"text_color": "Text Color",
"background_color": "Background Color",
"outline_color": "Outline Color",
"outline_thickness": "Outline Thickness",
"background_opacity": "Background Opacity",
"outline_opacity": "Outline Opacity",
"bold_text": "Bold Text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "None",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"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_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",
@@ -315,25 +277,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Other", "other_title": "Other",
"video_orientation": "Video Orientation", "video_orientation": "Video Orientation",
@@ -351,13 +294,7 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "Safe Area in Controls", "safe_area_in_controls": "Safe Area in Controls",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Show Custom Menu Links", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries", "hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback", "disable_haptic_feedback": "Disable Haptic Feedback",
@@ -367,9 +304,6 @@
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +347,18 @@
"read_more_about_marlin": "Read More About Marlin.", "read_more_about_marlin": "Read More About Marlin.",
"save_button": "Save", "save_button": "Save",
"toasts": { "toasts": {
"saved": "Saved", "saved": "Saved"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable 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": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "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_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections",
"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",
@@ -445,8 +374,7 @@
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration"
"watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +385,6 @@
"delete_all_downloaded_files": "Delete All Downloaded Files", "delete_all_downloaded_files": "Delete All Downloaded Files",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +394,6 @@
"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": "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_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_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -481,8 +406,7 @@
"export_logs": "Export Logs", "export_logs": "Export Logs",
"click_for_more_info": "Click for More Info", "click_for_more_info": "Click for More Info",
"level": "Level", "level": "Level",
"no_logs_available": "No Logs Available", "no_logs_available": "No Logs Available"
"delete_all_logs": "Delete All Logs"
}, },
"languages": { "languages": {
"title": "Languages", "title": "Languages",
@@ -490,15 +414,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -518,10 +439,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "TV-Series", "tvseries": "TV-Series",
"movies": "Movies", "movies": "Movies",
"queue": "Queue",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Queue and downloads will be lost on app restart",
"no_items_in_queue": "No Items in Queue",
"no_downloaded_items": "No Downloaded Items", "no_downloaded_items": "No Downloaded Items",
"delete_all_movies_button": "Delete All Movies", "delete_all_movies_button": "Delete All Movies",
"delete_all_tvseries_button": "Delete All TV-Series", "delete_all_tvseries_button": "Delete All TV-Series",
@@ -546,13 +464,8 @@
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
"deleted_media_successfully": "Deleted other media Successfully!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "Download Deleted",
"download_cancelled": "Download Cancelled", "download_cancelled": "Download Cancelled",
"could_not_delete_download": "Could Not Delete Download", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "Download Paused",
"could_not_pause_download": "Could Not Pause Download",
"download_resumed": "Download Resumed",
"could_not_resume_download": "Could Not Resume Download",
"download_completed": "Download Completed", "download_completed": "Download Completed",
"download_failed": "Download Failed", "download_failed": "Download Failed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}", "download_failed_for_item": "Download failed for {{item}} - {{error}}",
@@ -562,10 +475,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All Downloads Deleted Successfully", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +493,13 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login"
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -691,10 +598,6 @@
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode", "next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching", "continue_watching": "Continue Watching",
"go_back": "Go Back", "go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -761,7 +664,6 @@
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left", "left": "left",
"more_info": "More Info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
@@ -888,13 +790,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +926,6 @@
"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",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -1,17 +0,0 @@
/**
* Countdown state for Chromecast next-episode autoplay. The watcher
* (`useCastAutoplay`) writes it; the casting-player overlay reads it.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { atom } from "jotai";
export interface CastAutoplayState {
/** The episode queued to play next. */
nextEpisode: BaseItemDto;
/** Seconds left before it loads. */
secondsRemaining: number;
}
/** Active cast autoplay countdown, or null when none is running. */
export const castAutoplayAtom = atom<CastAutoplayState | null>(null);

View File

@@ -6,13 +6,11 @@ import {
type SortOrder, type SortOrder,
SubtitlePlaybackMode, SubtitlePlaybackMode,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { t } from "i18next";
import { atom, useAtom, useAtomValue } from "jotai"; import { atom, useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import type { ChromecastProfileMode } from "@/utils/casting/capabilities";
import { writeInfoLog } from "@/utils/log"; import { writeInfoLog } from "@/utils/log";
import { storage } from "../mmkv"; import { storage } from "../mmkv";
@@ -123,46 +121,6 @@ export interface MaxAutoPlayEpisodeCount {
value: number; value: number;
} }
/**
* The plugin may send object-typed settings as plain primitives.
* Resolve to the proper option object from the available choices.
*/
const normalizePluginValue = (
settingsKey: keyof Settings,
value: unknown,
): unknown => {
if (typeof value !== "object" || value === null) {
const defaultVal = defaultValues[settingsKey];
if (
typeof defaultVal === "object" &&
defaultVal !== null &&
"key" in defaultVal &&
"value" in defaultVal
) {
// defaultBitrate needs a lookup because its keys are human-readable
// (e.g. "8 Mb/s") that can't be derived from the raw value (e.g. 8000000).
// Other { key, value } settings like maxAutoPlayEpisodeCount work with
// the fallback because their keys are just String(value) (e.g. "5").
if (settingsKey === "defaultBitrate") {
const match = BITRATES.find(
(b) => b.key === value || b.value === value,
);
if (match) return match;
}
// maxAutoPlayEpisodeCount: 0 is invalid (breaks autoplay), clamp to -1
// -1 key must match the translated dropdown label so the UI shows "Disabled"
if (
settingsKey === "maxAutoPlayEpisodeCount" &&
(value === 0 || value === -1)
) {
return { key: t("home.settings.other.disabled"), value: -1 };
}
return { key: String(value), value };
}
}
return value;
};
export type HomeSectionLatestResolver = { export type HomeSectionLatestResolver = {
parentId?: string; parentId?: string;
limit?: number; limit?: number;
@@ -176,9 +134,6 @@ export enum VideoPlayer {
MPV = 0, MPV = 0,
} }
// Segment skip behavior options
export type SegmentSkipMode = "none" | "ask" | "auto";
// TV Typography scale presets // TV Typography scale presets
export enum TVTypographyScale { export enum TVTypographyScale {
Small = "small", Small = "small",
@@ -246,23 +201,10 @@ export type Settings = {
jellyseerrServerUrl?: string; jellyseerrServerUrl?: string;
useKefinTweaks: boolean; useKefinTweaks: boolean;
hiddenLibraries?: string[]; hiddenLibraries?: string[];
/** Chromecast profile selection mode. "auto" detects per device. */ enableH265ForChromecast: boolean;
chromecastProfile: ChromecastProfileMode;
/** Optional manual Chromecast video bitrate cap, in bits per second. */
chromecastMaxBitrate?: number;
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount; maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
autoPlayEpisodeCount: number; autoPlayEpisodeCount: number;
autoPlayNextEpisode: boolean; autoPlayNextEpisode: boolean;
// Media segment skip preferences
skipIntro: SegmentSkipMode;
skipOutro: SegmentSkipMode;
skipRecap: SegmentSkipMode;
skipCommercial: SegmentSkipMode;
skipPreview: SegmentSkipMode;
/** Native player next-episode countdown, in seconds. */
autoplayCountdownSeconds: number;
/** Chromecast next-episode countdown, in seconds. */
castAutoplayCountdownSeconds: number;
// Playback speed settings // Playback speed settings
defaultPlaybackSpeed: number; defaultPlaybackSpeed: number;
playbackSpeedPerMedia: Record<string, number>; playbackSpeedPerMedia: Record<string, number>;
@@ -362,19 +304,10 @@ export const defaultValues: Settings = {
jellyseerrServerUrl: undefined, jellyseerrServerUrl: undefined,
useKefinTweaks: false, useKefinTweaks: false,
hiddenLibraries: [], hiddenLibraries: [],
chromecastProfile: "auto", enableH265ForChromecast: false,
chromecastMaxBitrate: undefined,
maxAutoPlayEpisodeCount: { key: "3", value: 3 }, maxAutoPlayEpisodeCount: { key: "3", value: 3 },
autoPlayEpisodeCount: 0, autoPlayEpisodeCount: 0,
autoPlayNextEpisode: true, autoPlayNextEpisode: true,
// Media segment skip defaults
skipIntro: "ask",
skipOutro: "ask",
skipRecap: "ask",
skipCommercial: "ask",
skipPreview: "ask",
autoplayCountdownSeconds: 15,
castAutoplayCountdownSeconds: 30,
// Playback speed defaults // Playback speed defaults
defaultPlaybackSpeed: 1.0, defaultPlaybackSpeed: 1.0,
playbackSpeedPerMedia: {}, playbackSpeedPerMedia: {},
@@ -494,7 +427,8 @@ export const useSettings = () => {
[_setPluginSettings], [_setPluginSettings],
); );
const refreshStreamyfinPluginSettings = useCallback(async () => { const refreshStreamyfinPluginSettings = useCallback(
async (forceOverride = false) => {
if (!api) { if (!api) {
return; return;
} }
@@ -507,16 +441,37 @@ export const useSettings = () => {
); );
setPluginSettings(newPluginSettings); setPluginSettings(newPluginSettings);
// Locked/unlocked values are handled by the settings memo, which // Apply plugin values to settings
// applies locked values at runtime without overwriting user storage.
// We only handle auto-enabling Streamystats here.
if (newPluginSettings && _settings) { if (newPluginSettings && _settings) {
const updates: Partial<Settings> = {};
for (const [key, setting] of Object.entries(newPluginSettings)) {
if (setting && !setting.locked && setting.value !== undefined) {
const settingsKey = key as keyof Settings;
const effectiveValue = getEffectiveSettingValue(
_settings,
settingsKey,
);
// Apply if forceOverride is true, or if neither persisted settings
// nor app defaults provide a meaningful value.
if (forceOverride || !hasMeaningfulSettingValue(effectiveValue)) {
(updates as any)[settingsKey] = setting.value;
}
}
}
// Auto-enable Streamystats if server URL is provided
const streamyStatsUrl = newPluginSettings.streamyStatsServerUrl; const streamyStatsUrl = newPluginSettings.streamyStatsServerUrl;
if (streamyStatsUrl?.value && _settings.searchEngine !== "Streamystats") { if (
streamyStatsUrl?.value &&
_settings.searchEngine !== "Streamystats"
) {
updates.searchEngine = "Streamystats";
}
if (Object.keys(updates).length > 0) {
const newSettings = { const newSettings = {
...defaultValues, ...defaultValues,
..._settings, ..._settings,
searchEngine: "Streamystats", ...updates,
} as Settings; } as Settings;
setSettings(newSettings); setSettings(newSettings);
saveSettings(newSettings); saveSettings(newSettings);
@@ -524,7 +479,9 @@ export const useSettings = () => {
} }
return newPluginSettings; return newPluginSettings;
}, [api, _settings]); },
[api, _settings],
);
const updateSettings = (update: Partial<Settings>) => { const updateSettings = (update: Partial<Settings>) => {
if (!_settings) { if (!_settings) {
@@ -555,13 +512,8 @@ export const useSettings = () => {
Partial<Settings> Partial<Settings>
>((acc, [key, setting]) => { >((acc, [key, setting]) => {
if (setting) { if (setting) {
let { value } = setting; const { value, locked } = setting;
const { locked } = setting;
const settingsKey = key as keyof Settings; const settingsKey = key as keyof Settings;
// Normalize object-typed settings from plugin (plain primitive → { key, value })
value = normalizePluginValue(settingsKey, value);
const effectiveValue = getEffectiveSettingValue(_settings, settingsKey); const effectiveValue = getEffectiveSettingValue(_settings, settingsKey);
(acc as any)[settingsKey] = locked (acc as any)[settingsKey] = locked

View File

@@ -1,55 +0,0 @@
import { describe, expect, test } from "bun:test";
import { buildChromecastProfile } from "./buildProfile";
import { CONSERVATIVE_CAPABILITIES } from "./capabilities";
describe("buildChromecastProfile", () => {
test("conservative caps produce an H.264-only video codec list", () => {
const profile = buildChromecastProfile(CONSERVATIVE_CAPABILITIES);
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
expect(videoCodecProfile?.Codec).toBe("h264");
});
test("HEVC-capable caps include hevc in the video codec list", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
hevc: true,
});
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
expect(videoCodecProfile?.Codec).toContain("hevc");
});
test("maxVideoBitrate drives MaxStreamingBitrate", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
maxVideoBitrate: 5_000_000,
});
expect(profile.MaxStreamingBitrate).toBe(5_000_000);
});
test("maxAudioChannels constrains transcoding profiles", () => {
const profile = buildChromecastProfile(CONSERVATIVE_CAPABILITIES);
const videoTranscode = profile.TranscodingProfiles?.find(
(p) => p.Type === "Video",
);
expect(videoTranscode?.MaxAudioChannels).toBe("2");
});
test("non-10bit HEVC caps add a video bit-depth condition", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
hevc: true,
hevc10bit: false,
});
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
const bitDepthCondition = videoCodecProfile?.Conditions?.find(
(cond) => cond.Property === "VideoBitDepth",
);
expect(bitDepthCondition).toBeDefined();
});
});

View File

@@ -1,106 +0,0 @@
import type {
DeviceProfile,
ProfileCondition,
} from "@jellyfin/sdk/lib/generated-client/models";
import type { ChromecastCapabilities } from "./capabilities";
/**
* Build a Jellyfin `DeviceProfile` for a Chromecast from its detected capabilities.
* Replaces the former static `chromecast.ts` / `chromecasth265.ts` profiles.
*/
export const buildChromecastProfile = (
caps: ChromecastCapabilities,
): DeviceProfile => {
const videoCodecs = caps.hevc ? "hevc,h264" : "h264";
const maxHeight = caps.maxResolution === 2160 ? "2160" : "1080";
const maxChannels = String(caps.maxAudioChannels);
const videoConditions: ProfileCondition[] = [
{
Condition: "LessThanEqual",
Property: "Height",
Value: maxHeight,
IsRequired: false,
},
];
// When HEVC is allowed but 10-bit is not, force the server to transcode
// 10-bit sources down to 8-bit.
if (caps.hevc && !caps.hevc10bit) {
videoConditions.push({
Condition: "LessThanEqual",
Property: "VideoBitDepth",
Value: "8",
IsRequired: false,
});
}
return {
Name: "Chromecast Video Profile",
MaxStreamingBitrate: caps.maxVideoBitrate,
MaxStaticBitrate: caps.maxVideoBitrate,
MusicStreamingTranscodingBitrate: 384000,
CodecProfiles: [
{
Type: "Video",
Codec: videoCodecs,
Conditions: videoConditions,
},
{
Type: "Audio",
Codec: "aac,mp3,flac,opus,vorbis",
// Force transcode of multichannel audio the receiver cannot output.
Conditions: [
{
Condition: "LessThanEqual",
Property: "AudioChannels",
Value: maxChannels,
IsRequired: false,
},
],
},
],
ContainerProfiles: [],
DirectPlayProfiles: [
{
Container: caps.hevc ? "mp4,mkv" : "mp4",
Type: "Video",
VideoCodec: videoCodecs,
AudioCodec: "aac,mp3,opus,vorbis",
},
{ Container: "mp3", Type: "Audio" },
{ Container: "aac", Type: "Audio" },
{ Container: "flac", Type: "Audio" },
{ Container: "wav", Type: "Audio" },
],
TranscodingProfiles: [
{
Container: "ts",
Type: "Video",
VideoCodec: videoCodecs,
AudioCodec: "aac,mp3",
Protocol: "hls",
Context: "Streaming",
MaxAudioChannels: maxChannels,
MinSegments: 2,
BreakOnNonKeyFrames: true,
},
{
Container: "mp3",
Type: "Audio",
AudioCodec: "mp3",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: maxChannels,
},
{
Container: "aac",
Type: "Audio",
AudioCodec: "aac",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: maxChannels,
},
],
SubtitleProfiles: [{ Format: "vtt", Method: "Encode" }],
};
};

Some files were not shown because too many files have changed in this diff Show More