mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-30 10:38:35 +01:00
Compare commits
10 Commits
fix/refres
...
I10n_crowd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec7f555bd | ||
|
|
0f86c776ba | ||
|
|
07b79de203 | ||
|
|
cf91c4c682 | ||
|
|
1545790528 | ||
|
|
11ec778bd8 | ||
|
|
0c6ef5cbda | ||
|
|
1e14c7ec46 | ||
|
|
c8ddb9a892 | ||
|
|
9ee71a002d |
16
.github/workflows/build-apps.yml
vendored
16
.github/workflows/build-apps.yml
vendored
@@ -318,12 +318,12 @@ jobs:
|
|||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
- name: 🍞 Setup Bun
|
- name: 🍞 Setup Bun
|
||||||
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
|
|
||||||
- name: 💾 Cache Bun dependencies
|
- name: 💾 Cache Bun dependencies
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.bun/install/cache
|
path: ~/.bun/install/cache
|
||||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||||
@@ -339,7 +339,7 @@ jobs:
|
|||||||
run: bun run prebuild:tv
|
run: bun run prebuild:tv
|
||||||
|
|
||||||
- name: 🔧 Setup Xcode
|
- name: 🔧 Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||||
with:
|
with:
|
||||||
xcode-version: "26.2"
|
xcode-version: "26.2"
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ jobs:
|
|||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 📤 Upload IPA artifact
|
- name: 📤 Upload IPA artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
|
name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
|
||||||
path: build-*.ipa
|
path: build-*.ipa
|
||||||
@@ -384,12 +384,12 @@ jobs:
|
|||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
- name: 🍞 Setup Bun
|
- name: 🍞 Setup Bun
|
||||||
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
|
|
||||||
- name: 💾 Cache Bun dependencies
|
- name: 💾 Cache Bun dependencies
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.bun/install/cache
|
path: ~/.bun/install/cache
|
||||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||||
@@ -405,7 +405,7 @@ jobs:
|
|||||||
run: bun run prebuild:tv
|
run: bun run prebuild:tv
|
||||||
|
|
||||||
- name: 🔧 Setup Xcode
|
- name: 🔧 Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||||
with:
|
with:
|
||||||
xcode-version: "26.2"
|
xcode-version: "26.2"
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ jobs:
|
|||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 📤 Upload IPA artifact
|
- name: 📤 Upload IPA artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }}
|
name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }}
|
||||||
path: build/*.ipa
|
path: build/*.ipa
|
||||||
|
|||||||
6
.github/workflows/ci-codeql.yml
vendored
6
.github/workflows/ci-codeql.yml
vendored
@@ -27,13 +27,13 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
- name: 🏁 Initialize CodeQL
|
||||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended,security-and-quality
|
queries: +security-extended,security-and-quality
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
- name: 🛠️ Autobuild
|
||||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
- name: 🧪 Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ const Page: React.FC = () => {
|
|||||||
ItemFields.MediaStreams,
|
ItemFields.MediaStreams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Lazily preload item with full media sources in background
|
// Lazily preload item with full media sources in background — never cache
|
||||||
const { data: itemWithSources } = useItemQuery(id, isOffline, undefined, []);
|
const { data: itemWithSources } = useItemQuery(id, isOffline, undefined, [], {
|
||||||
|
gcTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const opacity = useSharedValue(1);
|
const opacity = useSharedValue(1);
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
|||||||
@@ -375,8 +375,9 @@ function Layout() {
|
|||||||
maxAge: 1000 * 60 * 60 * 24, // 24 hours max cache age
|
maxAge: 1000 * 60 * 60 * 24, // 24 hours max cache age
|
||||||
dehydrateOptions: {
|
dehydrateOptions: {
|
||||||
shouldDehydrateQuery: (query) => {
|
shouldDehydrateQuery: (query) => {
|
||||||
// Only persist successful queries
|
return (
|
||||||
return query.state.status === "success";
|
query.state.status === "success" && query.options.gcTime !== 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { type Href } from "expo-router";
|
import { type Href } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@@ -195,9 +196,30 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const downloadDetailsPromises = items.map(async (item) => {
|
const downloadDetailsPromises = items.map(async (item) => {
|
||||||
|
// Ensure the snapshot we store offline carries the Chapters array.
|
||||||
|
// Page-level fetches sometimes use a fields filter that omits it; the
|
||||||
|
// offline player would then render no chapter ticks / list.
|
||||||
|
let itemForDownload = item;
|
||||||
|
if (!itemForDownload.Chapters && itemForDownload.Id) {
|
||||||
|
try {
|
||||||
|
const enriched = await getUserLibraryApi(api).getItem({
|
||||||
|
itemId: itemForDownload.Id,
|
||||||
|
userId: user.Id!,
|
||||||
|
});
|
||||||
|
if (enriched.data) {
|
||||||
|
itemForDownload = enriched.data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
"[DownloadItem] failed to refresh item for Chapters, falling back to original",
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { mediaSource, audioIndex, subtitleIndex } =
|
const { mediaSource, audioIndex, subtitleIndex } =
|
||||||
itemsNotDownloaded.length > 1
|
itemsNotDownloaded.length > 1
|
||||||
? getDefaultPlaySettings(item, settings!)
|
? getDefaultPlaySettings(itemForDownload, settings!)
|
||||||
: {
|
: {
|
||||||
mediaSource: selectedOptions?.mediaSource,
|
mediaSource: selectedOptions?.mediaSource,
|
||||||
audioIndex: selectedOptions?.audioIndex,
|
audioIndex: selectedOptions?.audioIndex,
|
||||||
@@ -206,7 +228,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
|
|
||||||
const downloadDetails = await getDownloadUrl({
|
const downloadDetails = await getDownloadUrl({
|
||||||
api,
|
api,
|
||||||
item,
|
item: itemForDownload,
|
||||||
userId: user.Id!,
|
userId: user.Id!,
|
||||||
mediaSource: mediaSource!,
|
mediaSource: mediaSource!,
|
||||||
audioStreamIndex: audioIndex ?? -1,
|
audioStreamIndex: audioIndex ?? -1,
|
||||||
@@ -218,7 +240,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
url: downloadDetails?.url,
|
url: downloadDetails?.url,
|
||||||
item,
|
item: itemForDownload,
|
||||||
mediaSource: downloadDetails?.mediaSource,
|
mediaSource: downloadDetails?.mediaSource,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
196
components/chapters/ChapterList.tsx
Normal file
196
components/chapters/ChapterList.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* A modal listing an item's chapters. Each row shows the chapter name and its
|
||||||
|
* timestamp; the current chapter is highlighted. Tapping a row seeks to that
|
||||||
|
* chapter and closes the modal. Player-agnostic — the seek is injected.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { memo, useEffect, useMemo, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import {
|
||||||
|
type ChapterEntry,
|
||||||
|
chapterStartsMs,
|
||||||
|
formatChapterTime,
|
||||||
|
sortedChapters,
|
||||||
|
} from "@/utils/chapters";
|
||||||
|
|
||||||
|
interface ChapterListProps {
|
||||||
|
visible: boolean;
|
||||||
|
chapters: ChapterInfo[] | null | undefined;
|
||||||
|
/** Current playback position in milliseconds (to highlight the row). */
|
||||||
|
currentPositionMs: number;
|
||||||
|
/** Seek the player to this millisecond position. */
|
||||||
|
onSeek: (positionMs: number) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 48;
|
||||||
|
|
||||||
|
function ChapterListComponent({
|
||||||
|
visible,
|
||||||
|
chapters,
|
||||||
|
currentPositionMs,
|
||||||
|
onSeek,
|
||||||
|
onClose,
|
||||||
|
}: ChapterListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const listRef = useRef<FlatList<ChapterEntry>>(null);
|
||||||
|
|
||||||
|
const entries = useMemo(() => sortedChapters(chapters), [chapters]);
|
||||||
|
// Memoize starts so currentChapterIndex computation doesn't re-sort/filter
|
||||||
|
// every tick — chapters is the only input that drives the underlying array.
|
||||||
|
const starts = useMemo(() => chapterStartsMs(chapters), [chapters]);
|
||||||
|
const activeIndex = useMemo(() => {
|
||||||
|
let idx = -1;
|
||||||
|
for (let i = 0; i < starts.length; i++) {
|
||||||
|
if (currentPositionMs >= starts[i]) idx = i;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}, [currentPositionMs, starts]);
|
||||||
|
|
||||||
|
// FlatList.initialScrollIndex only fires at first mount; <Modal> keeps its
|
||||||
|
// children mounted across visible toggles, so subsequent opens never scroll.
|
||||||
|
// Trigger an imperative scroll each time the sheet becomes visible.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || activeIndex < 0 || entries.length === 0) return;
|
||||||
|
const raf = requestAnimationFrame(() => {
|
||||||
|
listRef.current?.scrollToIndex({
|
||||||
|
index: activeIndex,
|
||||||
|
animated: false,
|
||||||
|
viewPosition: 0.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(raf);
|
||||||
|
}, [visible, activeIndex, entries.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType='slide'
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<Pressable onPress={onClose} style={styles.backdrop}>
|
||||||
|
<Pressable onPress={(e) => e.stopPropagation()} style={styles.sheet}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>{t("chapters.title")}</Text>
|
||||||
|
<Pressable
|
||||||
|
onPress={onClose}
|
||||||
|
hitSlop={10}
|
||||||
|
accessibilityRole='button'
|
||||||
|
accessibilityLabel={t("chapters.close")}
|
||||||
|
>
|
||||||
|
<Ionicons name='close' size={24} color={Colors.text} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
ref={listRef}
|
||||||
|
data={entries}
|
||||||
|
keyExtractor={(item, index) => `${item.positionMs}-${index}`}
|
||||||
|
getItemLayout={(_, index) => ({
|
||||||
|
length: ROW_HEIGHT,
|
||||||
|
offset: ROW_HEIGHT * index,
|
||||||
|
index,
|
||||||
|
})}
|
||||||
|
onScrollToIndexFailed={(info) => {
|
||||||
|
// Required when getItemLayout is provided and the target index
|
||||||
|
// is outside the currently rendered window. Fallback to an
|
||||||
|
// offset-based scroll, then retry the precise scroll once a
|
||||||
|
// frame has elapsed.
|
||||||
|
listRef.current?.scrollToOffset({
|
||||||
|
offset: info.averageItemLength * info.index,
|
||||||
|
animated: false,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
listRef.current?.scrollToIndex({
|
||||||
|
index: info.index,
|
||||||
|
animated: false,
|
||||||
|
viewPosition: 0.5,
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}}
|
||||||
|
renderItem={({ item, index }) => {
|
||||||
|
const positionMs = item.positionMs;
|
||||||
|
const isActive = index === activeIndex;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
onSeek(positionMs);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
styles.row,
|
||||||
|
isActive && { backgroundColor: `${Colors.primary}33` },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.rowText,
|
||||||
|
{ color: isActive ? Colors.primary : Colors.text },
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{item.chapter.Name ||
|
||||||
|
t("chapters.chapter_number", { number: index + 1 })}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.rowTime}>
|
||||||
|
{formatChapterTime(positionMs)}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
</Pressable>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChapterList = memo(ChapterListComponent);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
backdrop: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.6)",
|
||||||
|
},
|
||||||
|
sheet: {
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
maxHeight: "70%",
|
||||||
|
paddingBottom: 24,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: Colors.text,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "700",
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
height: ROW_HEIGHT,
|
||||||
|
},
|
||||||
|
rowText: {
|
||||||
|
fontSize: 15,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
rowTime: {
|
||||||
|
color: Colors.icon,
|
||||||
|
fontSize: 13,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
87
components/chapters/ChapterTicks.tsx
Normal file
87
components/chapters/ChapterTicks.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Chapter tick marks drawn as an absolute overlay over a progress slider.
|
||||||
|
* Renders nothing for media with one or zero chapters. `pointerEvents: "none"`
|
||||||
|
* so the slider underneath still receives touches.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { memo, useState } from "react";
|
||||||
|
import { type LayoutChangeEvent, PixelRatio, View } from "react-native";
|
||||||
|
import type { ChapterMarker } from "@/utils/chapters";
|
||||||
|
|
||||||
|
interface ChapterTicksProps {
|
||||||
|
/** Pre-computed markers (caller memoizes — avoids double-computing here). */
|
||||||
|
markers: ChapterMarker[];
|
||||||
|
/** Tick colour. */
|
||||||
|
color?: string;
|
||||||
|
/** Tick height in px — slightly less than the slider track thickness. */
|
||||||
|
height?: number;
|
||||||
|
/** Tick width in px — integer to avoid sub-pixel anti-aliasing. */
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChapterTicksComponent({
|
||||||
|
markers,
|
||||||
|
// Semi-transparent black contrasts against both the filled progress
|
||||||
|
// (#fff) and the unfilled track (rgba(255,255,255,0.2)) so the ticks
|
||||||
|
// stay visible across the whole bar as playback advances.
|
||||||
|
color = "rgba(0,0,0,0.55)",
|
||||||
|
height = 14,
|
||||||
|
width = 2,
|
||||||
|
}: ChapterTicksProps) {
|
||||||
|
// Hooks must run unconditionally — keep them before any early return.
|
||||||
|
const [sliderWidth, setSliderWidth] = useState(0);
|
||||||
|
|
||||||
|
const handleLayout = (e: LayoutChangeEvent) => {
|
||||||
|
setSliderWidth(e.nativeEvent.layout.width);
|
||||||
|
};
|
||||||
|
|
||||||
|
// One chapter (typically a single marker at 0) is not worth marking.
|
||||||
|
if (markers.length <= 1) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
pointerEvents='none'
|
||||||
|
onLayout={handleLayout}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
// Let ticks taller than this container bleed beyond its bounds.
|
||||||
|
overflow: "visible",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sliderWidth > 0 &&
|
||||||
|
markers
|
||||||
|
// Skip the leading 0ms marker — it overlaps the slider start and
|
||||||
|
// adds visual noise at an already-rendered boundary.
|
||||||
|
.filter((marker) => marker.positionMs > 0)
|
||||||
|
.map((marker, index) => {
|
||||||
|
// Align both the position AND the width onto the device's
|
||||||
|
// physical pixel grid. Without this, fractional dp values land
|
||||||
|
// at different sub-pixel fractions per tick — Android samples
|
||||||
|
// each one differently and some ticks render visibly thicker.
|
||||||
|
const centerDp = (marker.percent / 100) * sliderWidth;
|
||||||
|
const left = PixelRatio.roundToNearestPixel(centerDp - width / 2);
|
||||||
|
const snappedWidth = PixelRatio.roundToNearestPixel(width);
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`${marker.positionMs}-${index}`}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left,
|
||||||
|
top: "50%",
|
||||||
|
marginTop: -height / 2,
|
||||||
|
height,
|
||||||
|
width: snappedWidth,
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChapterTicks = memo(ChapterTicksComponent);
|
||||||
@@ -1,18 +1,34 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { FC } from "react";
|
import type {
|
||||||
import { View } from "react-native";
|
BaseItemDto,
|
||||||
|
ChapterInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { type FC, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Pressable, View } from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import { type SharedValue } from "react-native-reanimated";
|
import { type SharedValue } from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { ChapterList } from "@/components/chapters/ChapterList";
|
||||||
|
import { ChapterTicks } from "@/components/chapters/ChapterTicks";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
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";
|
||||||
|
|
||||||
|
// Chapter tick height in dp — matches the slider track height for a clean,
|
||||||
|
// flush look (no top/bottom overflow).
|
||||||
|
const TICK_HEIGHT = 10;
|
||||||
|
|
||||||
interface BottomControlsProps {
|
interface BottomControlsProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
|
/** Item chapters, used for the tick overlay and chapter list. */
|
||||||
|
chapters?: ChapterInfo[] | null;
|
||||||
|
/** Total media duration in milliseconds. */
|
||||||
|
durationMs: number;
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
isSliding: boolean;
|
isSliding: boolean;
|
||||||
showRemoteBubble: boolean;
|
showRemoteBubble: boolean;
|
||||||
@@ -38,6 +54,8 @@ interface BottomControlsProps {
|
|||||||
handleSliderChange: (value: number) => void;
|
handleSliderChange: (value: number) => void;
|
||||||
handleTouchStart: () => void;
|
handleTouchStart: () => void;
|
||||||
handleTouchEnd: () => void;
|
handleTouchEnd: () => void;
|
||||||
|
/** Programmatic seek (chapter list, hotkeys) — bypasses slide gesture state. */
|
||||||
|
seekTo: (value: number) => void;
|
||||||
|
|
||||||
// Trickplay props
|
// Trickplay props
|
||||||
trickPlayUrl: {
|
trickPlayUrl: {
|
||||||
@@ -61,6 +79,8 @@ interface BottomControlsProps {
|
|||||||
|
|
||||||
export const BottomControls: FC<BottomControlsProps> = ({
|
export const BottomControls: FC<BottomControlsProps> = ({
|
||||||
item,
|
item,
|
||||||
|
chapters,
|
||||||
|
durationMs,
|
||||||
showControls,
|
showControls,
|
||||||
isSliding,
|
isSliding,
|
||||||
showRemoteBubble,
|
showRemoteBubble,
|
||||||
@@ -84,12 +104,38 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
handleSliderChange,
|
handleSliderChange,
|
||||||
handleTouchStart,
|
handleTouchStart,
|
||||||
handleTouchEnd,
|
handleTouchEnd,
|
||||||
|
seekTo,
|
||||||
trickPlayUrl,
|
trickPlayUrl,
|
||||||
trickplayInfo,
|
trickplayInfo,
|
||||||
time,
|
time,
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const [chapterListVisible, setChapterListVisible] = useState(false);
|
||||||
|
|
||||||
|
// Only expose chapter UI when there are at least two real markers.
|
||||||
|
const chapterMarkerList = useMemo(
|
||||||
|
() => chapterMarkers(chapters, durationMs),
|
||||||
|
[chapters, durationMs],
|
||||||
|
);
|
||||||
|
const hasChapters = chapterMarkerList.length > 1;
|
||||||
|
|
||||||
|
// Current chapter name for the always-visible header label (live playback).
|
||||||
|
const currentChapterName = useMemo(
|
||||||
|
() => (hasChapters ? chapterNameAt(currentTime, chapters) : null),
|
||||||
|
[hasChapters, currentTime, chapters],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chapter name at the scrubbed position for the trickplay bubble. `time` is
|
||||||
|
// an {h,m,s} object derived from the slider's dragged value — convert back
|
||||||
|
// to ms for the lookup. Only useful while actively scrubbing.
|
||||||
|
const scrubChapterName = useMemo(() => {
|
||||||
|
if (!hasChapters) return null;
|
||||||
|
const scrubMs =
|
||||||
|
(time.hours * 3600 + time.minutes * 60 + time.seconds) * 1000;
|
||||||
|
return chapterNameAt(scrubMs, chapters);
|
||||||
|
}, [hasChapters, time.hours, time.minutes, time.seconds, chapters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -131,8 +177,24 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
{item?.Type === "Audio" && (
|
{item?.Type === "Audio" && (
|
||||||
<Text className='text-xs opacity-50'>{item?.Album}</Text>
|
<Text className='text-xs opacity-50'>{item?.Album}</Text>
|
||||||
)}
|
)}
|
||||||
|
{currentChapterName ? (
|
||||||
|
<Text className='text-xs opacity-70 mt-1' numberOfLines={1}>
|
||||||
|
{currentChapterName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className='flex flex-row space-x-2 shrink-0'>
|
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
||||||
|
{hasChapters && (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setChapterListVisible(true)}
|
||||||
|
hitSlop={10}
|
||||||
|
className='justify-center mr-4'
|
||||||
|
accessibilityRole='button'
|
||||||
|
accessibilityLabel={t("chapters.open")}
|
||||||
|
>
|
||||||
|
<Ionicons name='bookmarks' size={24} color='white' />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
<SkipButton
|
<SkipButton
|
||||||
showButton={showSkipButton}
|
showButton={showSkipButton}
|
||||||
onPress={skipIntro}
|
onPress={skipIntro}
|
||||||
@@ -176,6 +238,9 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
height: 10,
|
height: 10,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
|
// Allow chapter ticks taller than the 10px track to bleed out
|
||||||
|
// top/bottom (RN defaults to overflow: "hidden" on Android).
|
||||||
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
onTouchEnd={handleTouchEnd}
|
onTouchEnd={handleTouchEnd}
|
||||||
@@ -203,6 +268,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
trickPlayUrl={trickPlayUrl}
|
trickPlayUrl={trickPlayUrl}
|
||||||
trickplayInfo={trickplayInfo}
|
trickplayInfo={trickplayInfo}
|
||||||
time={time}
|
time={time}
|
||||||
|
chapterName={scrubChapterName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -212,6 +278,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
minimumValue={min}
|
minimumValue={min}
|
||||||
maximumValue={max}
|
maximumValue={max}
|
||||||
/>
|
/>
|
||||||
|
<ChapterTicks markers={chapterMarkerList} height={TICK_HEIGHT} />
|
||||||
</View>
|
</View>
|
||||||
<TimeDisplay
|
<TimeDisplay
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
@@ -219,6 +286,13 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<ChapterList
|
||||||
|
visible={chapterListVisible}
|
||||||
|
chapters={chapters}
|
||||||
|
currentPositionMs={currentTime}
|
||||||
|
onSeek={seekTo}
|
||||||
|
onClose={() => setChapterListVisible(false)}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const Controls: FC<Props> = ({
|
|||||||
handleTouchEnd,
|
handleTouchEnd,
|
||||||
handleSliderComplete,
|
handleSliderComplete,
|
||||||
handleSliderChange,
|
handleSliderChange,
|
||||||
|
seekTo,
|
||||||
} = useVideoSlider({
|
} = useVideoSlider({
|
||||||
progress,
|
progress,
|
||||||
isSeeking,
|
isSeeking,
|
||||||
@@ -528,6 +529,8 @@ export const Controls: FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<BottomControls
|
<BottomControls
|
||||||
item={item}
|
item={item}
|
||||||
|
chapters={item.Chapters}
|
||||||
|
durationMs={maxMs}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
isSliding={isSliding}
|
isSliding={isSliding}
|
||||||
showRemoteBubble={showRemoteBubble}
|
showRemoteBubble={showRemoteBubble}
|
||||||
@@ -551,6 +554,7 @@ export const Controls: FC<Props> = ({
|
|||||||
handleSliderChange={handleSliderChange}
|
handleSliderChange={handleSliderChange}
|
||||||
handleTouchStart={handleTouchStart}
|
handleTouchStart={handleTouchStart}
|
||||||
handleTouchEnd={handleTouchEnd}
|
handleTouchEnd={handleTouchEnd}
|
||||||
|
seekTo={seekTo}
|
||||||
trickPlayUrl={trickPlayUrl}
|
trickPlayUrl={trickPlayUrl}
|
||||||
trickplayInfo={trickplayInfo}
|
trickplayInfo={trickplayInfo}
|
||||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ interface TrickplayBubbleProps {
|
|||||||
minutes: number;
|
minutes: number;
|
||||||
seconds: number;
|
seconds: number;
|
||||||
};
|
};
|
||||||
|
/** Chapter name at the scrubbed position, if any. */
|
||||||
|
chapterName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
||||||
trickPlayUrl,
|
trickPlayUrl,
|
||||||
trickplayInfo,
|
trickplayInfo,
|
||||||
time,
|
time,
|
||||||
|
chapterName,
|
||||||
}) => {
|
}) => {
|
||||||
if (!trickPlayUrl || !trickplayInfo) {
|
if (!trickPlayUrl || !trickplayInfo) {
|
||||||
return null;
|
return null;
|
||||||
@@ -36,18 +39,30 @@ export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
|||||||
const { x, y, url } = trickPlayUrl;
|
const { x, y, url } = trickPlayUrl;
|
||||||
const tileWidth = CONTROLS_CONSTANTS.TILE_WIDTH;
|
const tileWidth = CONTROLS_CONSTANTS.TILE_WIDTH;
|
||||||
const tileHeight = tileWidth / trickplayInfo.aspectRatio!;
|
const tileHeight = tileWidth / trickplayInfo.aspectRatio!;
|
||||||
|
const timeStr = `${time.hours > 0 ? `${time.hours}:` : ""}${
|
||||||
|
time.minutes < 10 ? `0${time.minutes}` : time.minutes
|
||||||
|
}:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`;
|
||||||
|
|
||||||
|
// Slightly larger preview than before (scale 1.6 vs old 1.4) to give the
|
||||||
|
// overlay text more room and feel closer to the Jellyfin web style.
|
||||||
|
const previewScale = 1.6;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: -62,
|
left: -62,
|
||||||
|
// Sit just above the slider — high enough not to overlap the
|
||||||
|
// progress bar, low enough to feel anchored to the thumb.
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
paddingTop: 30,
|
paddingTop: 12,
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
width: tileWidth * 1.5,
|
width: tileWidth * 1.5,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
// Bring the bubble in front of the player title / overlays.
|
||||||
|
zIndex: 999,
|
||||||
|
elevation: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@@ -55,7 +70,7 @@ export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
|||||||
width: tileWidth,
|
width: tileWidth,
|
||||||
height: tileHeight,
|
height: tileHeight,
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
transform: [{ scale: 1.4 }],
|
transform: [{ scale: previewScale }],
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
}}
|
}}
|
||||||
className='bg-neutral-800 overflow-hidden'
|
className='bg-neutral-800 overflow-hidden'
|
||||||
@@ -75,17 +90,51 @@ export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
|||||||
source={{ uri: url }}
|
source={{ uri: url }}
|
||||||
contentFit='cover'
|
contentFit='cover'
|
||||||
/>
|
/>
|
||||||
|
{/*
|
||||||
|
* Bottom-right overlay (Jellyfin web style) — chapter name (small,
|
||||||
|
* faded) above the timestamp (small, bold). Sits on top of the
|
||||||
|
* trickplay frame inside the same overflow:hidden container so it
|
||||||
|
* always stays within the bubble bounds.
|
||||||
|
*/}
|
||||||
|
<View
|
||||||
|
pointerEvents='none'
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 4,
|
||||||
|
bottom: 3,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
paddingHorizontal: 3,
|
||||||
|
paddingVertical: 1,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
maxWidth: tileWidth - 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chapterName ? (
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 7,
|
||||||
|
opacity: 0.85,
|
||||||
|
lineHeight: 9,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chapterName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: "600",
|
||||||
|
lineHeight: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timeStr}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
marginTop: 30,
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${time.hours > 0 ? `${time.hours}:` : ""}${
|
|
||||||
time.minutes < 10 ? `0${time.minutes}` : time.minutes
|
|
||||||
}:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ export function useVideoSlider({
|
|||||||
[seek, play, progress, isSeeking],
|
[seek, play, progress, isSeeking],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Programmatic seek (chapter list, hotkeys) that bypasses the slide gesture.
|
||||||
|
// Reads `isPlaying` directly instead of `wasPlayingRef`, which is only set
|
||||||
|
// during a real slide and would carry stale state on a tap-to-seek.
|
||||||
|
const seekTo = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
const seekValue = Math.max(0, Math.floor(value));
|
||||||
|
progress.value = seekValue;
|
||||||
|
seek(seekValue);
|
||||||
|
if (isPlaying) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[seek, play, progress, isPlaying],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSliderChange = useCallback(
|
const handleSliderChange = useCallback(
|
||||||
debounce((value: number) => {
|
debounce((value: number) => {
|
||||||
// Convert ms to ticks for trickplay
|
// Convert ms to ticks for trickplay
|
||||||
@@ -96,5 +111,6 @@ export function useVideoSlider({
|
|||||||
handleTouchEnd,
|
handleTouchEnd,
|
||||||
handleSliderComplete,
|
handleSliderComplete,
|
||||||
handleSliderChange,
|
handleSliderChange,
|
||||||
|
seekTo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ export const excludeFields = (fieldsToExclude: ItemFields[]) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExtraQueryOptions = {
|
||||||
|
gcTime?: number;
|
||||||
|
staleTime?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const useItemQuery = (
|
export const useItemQuery = (
|
||||||
itemId: string | undefined,
|
itemId: string | undefined,
|
||||||
isOffline?: boolean,
|
isOffline?: boolean,
|
||||||
fields?: ItemFields[],
|
fields?: ItemFields[],
|
||||||
excludeFields?: ItemFields[],
|
excludeFields?: ItemFields[],
|
||||||
|
queryOptions?: ExtraQueryOptions,
|
||||||
) => {
|
) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
@@ -53,5 +59,6 @@ export const useItemQuery = (
|
|||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
refetchOnReconnect: true,
|
refetchOnReconnect: true,
|
||||||
networkMode: "always",
|
networkMode: "always",
|
||||||
|
...queryOptions,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gorhom/bottom-sheet": "5.2.8",
|
"@gorhom/bottom-sheet": "5.2.8",
|
||||||
"@jellyfin/sdk": "^0.13.0",
|
"@jellyfin/sdk": "^0.13.0",
|
||||||
"@react-native-community/netinfo": "^11.4.1",
|
"@react-native-community/netinfo": "^12.0.0",
|
||||||
"@react-navigation/material-top-tabs": "7.4.9",
|
"@react-navigation/material-top-tabs": "7.4.9",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@shopify/flash-list": "2.0.2",
|
"@shopify/flash-list": "2.0.2",
|
||||||
|
|||||||
@@ -30,48 +30,48 @@
|
|||||||
"connect_button": "اتصل",
|
"connect_button": "اتصل",
|
||||||
"previous_servers": "الخوادم السابقة",
|
"previous_servers": "الخوادم السابقة",
|
||||||
"clear_button": "مسح",
|
"clear_button": "مسح",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "مرر للإزالة",
|
||||||
"search_for_local_servers": "البحث عن الخوادم المحلية",
|
"search_for_local_servers": "البحث عن الخوادم المحلية",
|
||||||
"searching": "جاري البحث...",
|
"searching": "جاري البحث...",
|
||||||
"servers": "الخوادم",
|
"servers": "الخوادم",
|
||||||
"saved": "Saved",
|
"saved": "تم الحفظ",
|
||||||
"session_expired": "Session Expired",
|
"session_expired": "انتهت الجلسة",
|
||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "انتهت مدة صلاحية جلستك. الرجاء تسجيل الدخول مرة أخرى.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "إزالة تسجيل دخول محفوظ",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "سيؤدي هذا إلى إزالة بيانات تسجيل الدخول الخاص بك المحفوظة لهذا الخادم. ستحتاج إلى إدخال اسم المستخدم وكلمة المرور مرة أخرى في المرة القادمة.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "الحسابات {{count}}",
|
||||||
"select_account": "Select Account",
|
"select_account": "اختر الحساب",
|
||||||
"add_account": "Add Account",
|
"add_account": "إضافة حساب",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "سيؤدي هذا إلى إزالة بيانات تسجيل الدخول لـ {{username}}."
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "حفظ الحساب",
|
||||||
"save_for_later": "Save this account",
|
"save_for_later": "حفظ هذا الحساب",
|
||||||
"security_option": "Security Option",
|
"security_option": "خيارات الأمان",
|
||||||
"no_protection": "No protection",
|
"no_protection": "بدون حماية",
|
||||||
"no_protection_desc": "Quick login without authentication",
|
"no_protection_desc": "تسجيل دخول سريع بدون مصادقة",
|
||||||
"pin_code": "PIN code",
|
"pin_code": "رمز PIN",
|
||||||
"pin_code_desc": "4-digit PIN required when switching",
|
"pin_code_desc": "رمز PIN مكون من 4 أرقام مطلوب عند التبديل",
|
||||||
"password": "Re-enter password",
|
"password": "أعد إدخال كلمة المرور",
|
||||||
"password_desc": "Password required when switching",
|
"password_desc": "كلمة المرور مطلوبة عند التبديل",
|
||||||
"save_button": "Save",
|
"save_button": "حفظ",
|
||||||
"cancel_button": "Cancel"
|
"cancel_button": "إلغاء"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "أدخل رمز PIN",
|
||||||
"enter_pin_for": "Enter PIN for {{username}}",
|
"enter_pin_for": "أدخل رمز PIN لـ {{username}}",
|
||||||
"enter_4_digits": "Enter 4 digits",
|
"enter_4_digits": "ادخل 4 أرقام",
|
||||||
"invalid_pin": "Invalid PIN",
|
"invalid_pin": "PIN غير صالح",
|
||||||
"setup_pin": "Set Up PIN",
|
"setup_pin": "تعيين رمز PIN",
|
||||||
"confirm_pin": "Confirm PIN",
|
"confirm_pin": "تأكيد رمز PIN",
|
||||||
"pins_dont_match": "PINs don't match",
|
"pins_dont_match": "رموز PIN غير متطابقة",
|
||||||
"forgot_pin": "Forgot PIN?",
|
"forgot_pin": "نسيت رمز PIN؟",
|
||||||
"forgot_pin_desc": "Your saved credentials will be removed"
|
"forgot_pin_desc": "سيتم إزالة بيانات تسجيل الدخول المحفوظة الخاصة بك"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"enter_password": "Enter Password",
|
"enter_password": "أدخل كلمة المرور",
|
||||||
"enter_password_for": "Enter password for {{username}}",
|
"enter_password_for": "أدخل كلمة المرور لـ {{username}}",
|
||||||
"invalid_password": "Invalid password"
|
"invalid_password": "كلمة المرور غير صحيحة"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"checking_server_connection": "التحقق من اتصال الخادم...",
|
"checking_server_connection": "التحقق من اتصال الخادم...",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.",
|
"error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.",
|
||||||
"continue_watching": "متابعة المشاهدة",
|
"continue_watching": "متابعة المشاهدة",
|
||||||
"next_up": "التالي",
|
"next_up": "التالي",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "تابع و التالي",
|
||||||
"recently_added_in": "أضيف مؤخراً في {{libraryName}}",
|
"recently_added_in": "أضيف مؤخراً في {{libraryName}}",
|
||||||
"suggested_movies": "أفلام مقترحة",
|
"suggested_movies": "أفلام مقترحة",
|
||||||
"suggested_episodes": "حلقات مقترحة",
|
"suggested_episodes": "حلقات مقترحة",
|
||||||
@@ -120,36 +120,36 @@
|
|||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "المظهر",
|
"title": "المظهر",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "دمج تابع المشاهدة والتالي",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "إخفاء زر جلسة البث عن بُعد"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "الشبكة",
|
||||||
"local_network": "Local Network",
|
"local_network": "الشبكة المحلية",
|
||||||
"auto_switch_enabled": "Auto-switch when at home",
|
"auto_switch_enabled": "التبديل التلقائي عند المنزل",
|
||||||
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
|
"auto_switch_description": "التبديل تلقائياً إلى رابط URL محلي عند الاتصال بشبكة WiFi المنزلية",
|
||||||
"local_url": "Local URL",
|
"local_url": "رابط محلي",
|
||||||
"local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "أدخل عنوان الخادم المحلي الخاص بك (على سبيل المثال http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Home WiFi Networks",
|
"home_wifi_networks": "شبكات WiFi المنزل",
|
||||||
"add_current_network": "Add \"{{ssid}}\"",
|
"add_current_network": "إضافة \"{{ssid}}\"",
|
||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "غير متصل بشبكة WiFi",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "لا توجد شبكات مكونة",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "إضافة شبكة WiFi المنزلية الخاصة بك لتمكين التبديل التلقائي",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "شبكة WiFi الحالية",
|
||||||
"using_url": "Using",
|
"using_url": "استخدام",
|
||||||
"local": "Local URL",
|
"local": "رابط محلي",
|
||||||
"remote": "Remote URL",
|
"remote": "الـ URL الخارجي",
|
||||||
"not_connected": "Not connected",
|
"not_connected": "غير متصل",
|
||||||
"current_server": "Current Server",
|
"current_server": "الخادم الحالي",
|
||||||
"remote_url": "Remote URL",
|
"remote_url": "الـ URL الخارجي",
|
||||||
"active_url": "Active URL",
|
"active_url": "الرابط النشط",
|
||||||
"not_configured": "Not configured",
|
"not_configured": "لم يتم تكوينه",
|
||||||
"network_added": "Network added",
|
"network_added": "تمت إضافة الشبكة",
|
||||||
"network_already_added": "Network already added",
|
"network_already_added": "الشبكة مضافة مسبقاً",
|
||||||
"no_wifi_connected": "Not connected to WiFi",
|
"no_wifi_connected": "غير متصل بشبكة WiFi",
|
||||||
"permission_denied": "Location permission denied",
|
"permission_denied": "تم رفض إذن الوصول إلى الموقع",
|
||||||
"permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
|
"permission_denied_explanation": "يتطلب التعرف على شبكة WiFi للتبديل التلقائي الحصول على إذن الوصول إلى الموقع. يرجى تفعيله من الإعدادات."
|
||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "معلومات المستخدم",
|
"user_info_title": "معلومات المستخدم",
|
||||||
@@ -182,10 +182,10 @@
|
|||||||
"left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع",
|
"left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع",
|
||||||
"right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن",
|
"right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن",
|
||||||
"right_side_volume_description": "اسحب لأعلى/لأسفل على الجانب الأيمن لضبط مستوى الصوت",
|
"right_side_volume_description": "اسحب لأعلى/لأسفل على الجانب الأيمن لضبط مستوى الصوت",
|
||||||
"hide_volume_slider": "Hide Volume Slider",
|
"hide_volume_slider": "إخفاء شريط مستوى الصوت",
|
||||||
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
"hide_volume_slider_description": "إخفاء شريط التحكم في مستوى الصوت في مشغل الفيديو",
|
||||||
"hide_brightness_slider": "Hide Brightness Slider",
|
"hide_brightness_slider": "إخفاء شريط السطوع",
|
||||||
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
"hide_brightness_slider_description": "إخفاء شريط التحكم في السطوع في مشغل الفيديو"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "الصوت",
|
"audio_title": "الصوت",
|
||||||
@@ -195,12 +195,12 @@
|
|||||||
"none": "لا شيء",
|
"none": "لا شيء",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"transcode_mode": {
|
"transcode_mode": {
|
||||||
"title": "Audio Transcoding",
|
"title": "تحويل ترميز الصوت",
|
||||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
"description": "يتحكم في كيفية التعامل مع الصوت المحيطي (7.1، TrueHD، DTS-HD)",
|
||||||
"auto": "Auto",
|
"auto": "تلقائي",
|
||||||
"stereo": "Force Stereo",
|
"stereo": "إجبار تشغيل ستيريو",
|
||||||
"5_1": "Allow 5.1",
|
"5_1": "السماح بـ 5.1",
|
||||||
"passthrough": "Passthrough"
|
"passthrough": "تمرير الصوت"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
@@ -251,29 +251,29 @@
|
|||||||
"Normal": "عادي",
|
"Normal": "عادي",
|
||||||
"Thick": "سميك"
|
"Thick": "سميك"
|
||||||
},
|
},
|
||||||
"subtitle_color": "Subtitle Color",
|
"subtitle_color": "لون الترجمة",
|
||||||
"subtitle_background_color": "Background Color",
|
"subtitle_background_color": "لون الخلفية",
|
||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "خط الترجمة",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "إعدادات KSPlayer",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "فك الترميز بواسطة الجهاز",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "استخدم تسريع العتاد لفك ترميز الفيديو. قم بتعطيله إذا واجهت مشكلات في التشغيل."
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "إعدادات ترجمة VLC",
|
||||||
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
|
"hint": "تخصيص مظهر الترجمة لمشغل VLC. تصبح التغييرات سارية المفعول عند التشغيل التالي.",
|
||||||
"text_color": "Text Color",
|
"text_color": "لون النص",
|
||||||
"background_color": "Background Color",
|
"background_color": "لون الخلفية",
|
||||||
"background_opacity": "Background Opacity",
|
"background_opacity": "شفافية الخلفية",
|
||||||
"outline_color": "Outline Color",
|
"outline_color": "لون إطار الخط",
|
||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "شفافية إطار الخط",
|
||||||
"outline_thickness": "Outline Thickness",
|
"outline_thickness": "سمك إطار الخط",
|
||||||
"bold": "Bold Text",
|
"bold": "خط عريض",
|
||||||
"margin": "Bottom Margin"
|
"margin": "الهامش السفلي"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "مشغل الفيديو",
|
||||||
"video_player": "Video Player",
|
"video_player": "مشغل الفيديو",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "اختر مشغل الفيديو الذي سيتم استخدامه على نظام iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -305,8 +305,8 @@
|
|||||||
"select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.",
|
"select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.",
|
||||||
"disable_haptic_feedback": "تعطيل ردود الفعل اللمسية",
|
"disable_haptic_feedback": "تعطيل ردود الفعل اللمسية",
|
||||||
"default_quality": "الجودة الافتراضية",
|
"default_quality": "الجودة الافتراضية",
|
||||||
"default_playback_speed": "Default Playback Speed",
|
"default_playback_speed": "سرعة التشغيل الافتراضية",
|
||||||
"auto_play_next_episode": "Auto-play Next Episode",
|
"auto_play_next_episode": "تشغيل الحلقة التالية تلقائياً",
|
||||||
"max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا",
|
"max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا",
|
||||||
"disabled": "معطل"
|
"disabled": "معطل"
|
||||||
},
|
},
|
||||||
@@ -314,15 +314,15 @@
|
|||||||
"downloads_title": "التنزيلات"
|
"downloads_title": "التنزيلات"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "الموسيقى",
|
||||||
"playback_title": "Playback",
|
"playback_title": "التشغيل",
|
||||||
"playback_description": "Configure how music is played.",
|
"playback_description": "ضبط كيفية تشغيل الموسيقى.",
|
||||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
"prefer_downloaded": "تفضيل الأغاني التي تم تنزيلها",
|
||||||
"caching_title": "Caching",
|
"caching_title": "التخزين المؤقت",
|
||||||
"caching_description": "Automatically cache upcoming tracks for smoother playback.",
|
"caching_description": "تخزين الأغاني التالية مؤقتاً تلقائياً لضمان تشغيل أكثر سلاسة.",
|
||||||
"lookahead_enabled": "Enable Look-Ahead Caching",
|
"lookahead_enabled": "تفعيل التخزين المؤقت الاستباقي",
|
||||||
"lookahead_count": "Tracks to Pre-cache",
|
"lookahead_count": "عدد الأغاني المراد تخزينها مسبقاً",
|
||||||
"max_cache_size": "Max Cache Size"
|
"max_cache_size": "الحد الأقصى لحجم التخزين المؤقت"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"plugins_title": "الإضافات",
|
"plugins_title": "الإضافات",
|
||||||
@@ -357,39 +357,39 @@
|
|||||||
"save_button": "حفظ",
|
"save_button": "حفظ",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "تم الحفظ",
|
"saved": "تم الحفظ",
|
||||||
"refreshed": "Settings refreshed from server"
|
"refreshed": "تم تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"streamystats": {
|
"streamystats": {
|
||||||
"enable_streamystats": "Enable Streamystats",
|
"enable_streamystats": "تفعيل Streamystats",
|
||||||
"disable_streamystats": "Disable Streamystats",
|
"disable_streamystats": "تعطيل Streamystats",
|
||||||
"enable_search": "Use for Search",
|
"enable_search": "استخدم للبحث",
|
||||||
"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": "أدخل رابط خادم Streamystats الخاص بك. يجب أن يتضمن الرابط البروتوكول http أو https مع رقم المنفذ اختيارياً.",
|
||||||
"read_more_about_streamystats": "Read More About Streamystats.",
|
"read_more_about_streamystats": "اقرأ المزيد عن Streamystats.",
|
||||||
"save_button": "Save",
|
"save_button": "حفظ",
|
||||||
"save": "Save",
|
"save": "حفظ",
|
||||||
"features_title": "Features",
|
"features_title": "المميزات",
|
||||||
"home_sections_title": "Home Sections",
|
"home_sections_title": "أقسام الرئيسية",
|
||||||
"enable_movie_recommendations": "Movie Recommendations",
|
"enable_movie_recommendations": "توصيات الأفلام",
|
||||||
"enable_series_recommendations": "Series Recommendations",
|
"enable_series_recommendations": "توصيات المسلسلات",
|
||||||
"enable_promoted_watchlists": "Promoted Watchlists",
|
"enable_promoted_watchlists": "قوائم مشاهدة مختارة",
|
||||||
"hide_watchlists_tab": "Hide Watchlists Tab",
|
"hide_watchlists_tab": "إخفاء تبويب قوائم المشاهدة",
|
||||||
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
|
"home_sections_hint": "إظهار التوصيات المخصصة وقوائم المشاهدة المختارة من Streamystats في الصفحة الرئيسية.",
|
||||||
"recommended_movies": "Recommended Movies",
|
"recommended_movies": "أفلام موصى بها",
|
||||||
"recommended_series": "Recommended Series",
|
"recommended_series": "مسلسلات موصى بها",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "Saved",
|
"saved": "تم الحفظ",
|
||||||
"refreshed": "Settings refreshed from server",
|
"refreshed": "تم تحديث الإعدادات من الخادم",
|
||||||
"disabled": "Streamystats disabled"
|
"disabled": "تم تعطيل Streamystats"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"kefinTweaks": {
|
"kefinTweaks": {
|
||||||
"watchlist_enabler": "Enable our Watchlist integration",
|
"watchlist_enabler": "تفعيل الربط مع قائمة المشاهدة الخاصة بنا",
|
||||||
"watchlist_button": "Toggle Watchlist integration"
|
"watchlist_button": "تبديل حالة ربط قائمة المشاهدة"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
@@ -398,15 +398,15 @@
|
|||||||
"device_usage": "الجهاز {{availableSpace}}%",
|
"device_usage": "الجهاز {{availableSpace}}%",
|
||||||
"size_used": "تم استخدام {{used}} من {{total}}",
|
"size_used": "تم استخدام {{used}} من {{total}}",
|
||||||
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
|
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
|
||||||
"music_cache_title": "Music Cache",
|
"music_cache_title": "التخزين المؤقت للموسيقى",
|
||||||
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
"music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال",
|
||||||
"enable_music_cache": "Enable Music Cache",
|
"enable_music_cache": "تمكين التخزين المؤقت للموسيقى",
|
||||||
"clear_music_cache": "Clear Music Cache",
|
"clear_music_cache": "مسح التخزين المؤقت للموسيقى",
|
||||||
"music_cache_size": "{{size}} cached",
|
"music_cache_size": "تم تخزين {{size}} مؤقتاً",
|
||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "تم مسح التخزين المؤقت للموسيقى",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "حذف جميع الأغاني التي تم تنزيلها",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "تم تنزيل {{size}}",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "تم حذف الأغاني التي تم تنزيلها"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "المقدمة",
|
"title": "المقدمة",
|
||||||
@@ -499,15 +499,15 @@
|
|||||||
"subtitle": "الترجمة",
|
"subtitle": "الترجمة",
|
||||||
"play": "تشغيل",
|
"play": "تشغيل",
|
||||||
"none": "لا شيء",
|
"none": "لا شيء",
|
||||||
"track": "Track",
|
"track": "أغنية",
|
||||||
"cancel": "Cancel",
|
"cancel": "إلغاء",
|
||||||
"delete": "Delete",
|
"delete": "حذف",
|
||||||
"ok": "OK",
|
"ok": "حسناً",
|
||||||
"remove": "Remove",
|
"remove": "إزالة",
|
||||||
"next": "Next",
|
"next": "التالي",
|
||||||
"back": "Back",
|
"back": "رجوع",
|
||||||
"continue": "Continue",
|
"continue": "متابعة",
|
||||||
"verifying": "Verifying..."
|
"verifying": "جارٍ التحقق..."
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "بحث...",
|
"search": "بحث...",
|
||||||
@@ -521,10 +521,10 @@
|
|||||||
"episodes": "حلقات",
|
"episodes": "حلقات",
|
||||||
"collections": "مجموعات",
|
"collections": "مجموعات",
|
||||||
"actors": "ممثلون",
|
"actors": "ممثلون",
|
||||||
"artists": "Artists",
|
"artists": "الفنانون",
|
||||||
"albums": "Albums",
|
"albums": "الألبومات",
|
||||||
"songs": "Songs",
|
"songs": "الأغاني",
|
||||||
"playlists": "Playlists",
|
"playlists": "قوائم التشغيل",
|
||||||
"request_movies": "طلب أفلام",
|
"request_movies": "طلب أفلام",
|
||||||
"request_series": "طلب مسلسلات",
|
"request_series": "طلب مسلسلات",
|
||||||
"recently_added": "أضيف مؤخرًا",
|
"recently_added": "أضيف مؤخرًا",
|
||||||
@@ -572,7 +572,7 @@
|
|||||||
"genres": "الأنواع",
|
"genres": "الأنواع",
|
||||||
"years": "السنوات",
|
"years": "السنوات",
|
||||||
"sort_by": "ترتيب حسب",
|
"sort_by": "ترتيب حسب",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "تصفية حسب",
|
||||||
"sort_order": "اتجاه الترتيب",
|
"sort_order": "اتجاه الترتيب",
|
||||||
"tags": "الوسوم"
|
"tags": "الوسوم"
|
||||||
}
|
}
|
||||||
@@ -604,11 +604,17 @@
|
|||||||
"index": "الفِهْرِس:",
|
"index": "الفِهْرِس:",
|
||||||
"continue_watching": "متابعة المشاهدة",
|
"continue_watching": "متابعة المشاهدة",
|
||||||
"go_back": "رجوع",
|
"go_back": "رجوع",
|
||||||
"downloaded_file_title": "You have this file downloaded",
|
"downloaded_file_title": "تم تنزيل هذا الملف",
|
||||||
"downloaded_file_message": "هل تريد تشغيل الملف الذي تم تنزيله؟",
|
"downloaded_file_message": "هل تريد تشغيل الملف الذي تم تنزيله؟",
|
||||||
"downloaded_file_yes": "نعم",
|
"downloaded_file_yes": "نعم",
|
||||||
"downloaded_file_no": "لا",
|
"downloaded_file_no": "لا",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "إلغاء"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "التالي",
|
"next_up": "التالي",
|
||||||
@@ -624,7 +630,7 @@
|
|||||||
"no_similar_items_found": "لم يتم العثور على عناصر مشابهة",
|
"no_similar_items_found": "لم يتم العثور على عناصر مشابهة",
|
||||||
"video": "فيديو",
|
"video": "فيديو",
|
||||||
"more_details": "المزيد من التفاصيل",
|
"more_details": "المزيد من التفاصيل",
|
||||||
"media_options": "Media Options",
|
"media_options": "خيارات الوسائط",
|
||||||
"quality": "الجودة",
|
"quality": "الجودة",
|
||||||
"audio": "الصوت",
|
"audio": "الصوت",
|
||||||
"subtitles": "الترجمة",
|
"subtitles": "الترجمة",
|
||||||
@@ -719,127 +725,127 @@
|
|||||||
"favorites": "المفضلة"
|
"favorites": "المفضلة"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "الموسيقى",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "الإقتراحات",
|
||||||
"albums": "Albums",
|
"albums": "الألبومات",
|
||||||
"artists": "Artists",
|
"artists": "الفنانون",
|
||||||
"playlists": "Playlists",
|
"playlists": "قوائم التشغيل",
|
||||||
"tracks": "tracks"
|
"tracks": "الأغاني"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"all": "All"
|
"all": "الكل"
|
||||||
},
|
},
|
||||||
"recently_added": "Recently Added",
|
"recently_added": "أضيف مؤخرًا",
|
||||||
"recently_played": "Recently Played",
|
"recently_played": "تم تشغيله مؤخرًا",
|
||||||
"frequently_played": "Frequently Played",
|
"frequently_played": "الأكثر تشغيلاً",
|
||||||
"explore": "Explore",
|
"explore": "اكتشف",
|
||||||
"top_tracks": "Top Tracks",
|
"top_tracks": "أفضل الأغاني",
|
||||||
"play": "Play",
|
"play": "تشغيل",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "ترتيب عشوائي",
|
||||||
"play_top_tracks": "Play Top Tracks",
|
"play_top_tracks": "تشغيل أفضل الأغاني",
|
||||||
"no_suggestions": "No suggestions available",
|
"no_suggestions": "لا توجد مقترحات متاحة",
|
||||||
"no_albums": "No albums found",
|
"no_albums": "لا توجد ألبومات",
|
||||||
"no_artists": "No artists found",
|
"no_artists": "لا يوجد فنانون",
|
||||||
"no_playlists": "No playlists found",
|
"no_playlists": "لا توجد قوائم تشغيل",
|
||||||
"album_not_found": "Album not found",
|
"album_not_found": "الألبوم غير موجود",
|
||||||
"artist_not_found": "Artist not found",
|
"artist_not_found": "الفنان غير موجود",
|
||||||
"playlist_not_found": "Playlist not found",
|
"playlist_not_found": "قائمة التشغيل غير موجودة",
|
||||||
"track_options": {
|
"track_options": {
|
||||||
"play_next": "Play Next",
|
"play_next": "تشغيل التالي",
|
||||||
"add_to_queue": "Add to Queue",
|
"add_to_queue": "إضافة إلى قائمة الانتظار",
|
||||||
"add_to_playlist": "Add to Playlist",
|
"add_to_playlist": "أضف إلى قائمة التشغيل",
|
||||||
"download": "Download",
|
"download": "تنزيل",
|
||||||
"downloaded": "Downloaded",
|
"downloaded": "تم التنزيل",
|
||||||
"downloading": "Downloading...",
|
"downloading": "جارٍ التنزيل...",
|
||||||
"cached": "Cached",
|
"cached": "تم التخزين مؤقتاً",
|
||||||
"delete_download": "Delete Download",
|
"delete_download": "حذف ملف التنزيل",
|
||||||
"delete_cache": "Remove from Cache",
|
"delete_cache": "إزالة من التخزين المؤقت",
|
||||||
"go_to_artist": "Go to Artist",
|
"go_to_artist": "انتقال إلى الفنان",
|
||||||
"go_to_album": "Go to Album",
|
"go_to_album": "انتقال إلى الألبوم",
|
||||||
"add_to_favorites": "Add to Favorites",
|
"add_to_favorites": "إضافة إلى المفضلة",
|
||||||
"remove_from_favorites": "Remove from Favorites",
|
"remove_from_favorites": "إزالة من المفضلة",
|
||||||
"remove_from_playlist": "Remove from Playlist"
|
"remove_from_playlist": "إزالة من قائمة التشغيل"
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
"create_playlist": "Create Playlist",
|
"create_playlist": "إنشاء قائمة التشغيل",
|
||||||
"playlist_name": "Playlist Name",
|
"playlist_name": "اسم قائمة التشغيل",
|
||||||
"enter_name": "Enter playlist name",
|
"enter_name": "أدخل اسم قائمة التشغيل",
|
||||||
"create": "Create",
|
"create": "إنشاء",
|
||||||
"search_playlists": "Search playlists...",
|
"search_playlists": "البحث عن قوائم التشغيل...",
|
||||||
"added_to": "Added to {{name}}",
|
"added_to": "تمت الإضافة إلى {{name}}",
|
||||||
"added": "Added to playlist",
|
"added": "تمت الإضافة إلى قائمة التشغيل",
|
||||||
"removed_from": "Removed from {{name}}",
|
"removed_from": "تمت الإزالة من {{name}}",
|
||||||
"removed": "Removed from playlist",
|
"removed": "تمت الازالة من قائمة التشغيل",
|
||||||
"created": "Playlist created",
|
"created": "تم إنشاء قائمة التشغيل",
|
||||||
"create_new": "Create New Playlist",
|
"create_new": "إنشاء قائمة تشغيل جديدة",
|
||||||
"failed_to_add": "Failed to add to playlist",
|
"failed_to_add": "فشلت الإضافة إلى قائمة التشغيل",
|
||||||
"failed_to_remove": "Failed to remove from playlist",
|
"failed_to_remove": "فشلت الإزالة من قائمة التشغيل",
|
||||||
"failed_to_create": "Failed to create playlist",
|
"failed_to_create": "فشل إنشاء قائمة التشغيل",
|
||||||
"delete_playlist": "Delete Playlist",
|
"delete_playlist": "حذف قائمة التشغيل",
|
||||||
"delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
"delete_confirm": "هل أنت متأكد من رغبتك في حذف {{name}}؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
"deleted": "Playlist deleted",
|
"deleted": "تم حذف قائمة التشغيل",
|
||||||
"failed_to_delete": "Failed to delete playlist"
|
"failed_to_delete": "فشل إنشاء قائمة التشغيل"
|
||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
"title": "Sort By",
|
"title": "ترتيب حسب",
|
||||||
"alphabetical": "Alphabetical",
|
"alphabetical": "أبجدي",
|
||||||
"date_created": "Date Created"
|
"date_created": "تاريخ الإنشاء"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watchlists": {
|
"watchlists": {
|
||||||
"title": "Watchlists",
|
"title": "قوائم المشاهدة",
|
||||||
"my_watchlists": "My Watchlists",
|
"my_watchlists": "قوائم المشاهدة الخاصة بي",
|
||||||
"public_watchlists": "Public Watchlists",
|
"public_watchlists": "قوائم مشاهدة عامة",
|
||||||
"create_title": "Create Watchlist",
|
"create_title": "إنشاء قائمة مشاهدة",
|
||||||
"edit_title": "Edit Watchlist",
|
"edit_title": "تعديل قائمة المشاهدة",
|
||||||
"create_button": "Create Watchlist",
|
"create_button": "إنشاء قائمة مشاهدة",
|
||||||
"save_button": "Save Changes",
|
"save_button": "حفظ التغييرات",
|
||||||
"delete_button": "Delete",
|
"delete_button": "حذف",
|
||||||
"remove_button": "Remove",
|
"remove_button": "إزالة",
|
||||||
"cancel_button": "Cancel",
|
"cancel_button": "إلغاء",
|
||||||
"name_label": "Name",
|
"name_label": "الاسم",
|
||||||
"name_placeholder": "Enter watchlist name",
|
"name_placeholder": "أدخل اسم قائمة المشاهدة",
|
||||||
"description_label": "Description",
|
"description_label": "الوصف",
|
||||||
"description_placeholder": "Enter description (optional)",
|
"description_placeholder": "أدخل الوصف (اختياري)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "قائمة مشاهدة عامة",
|
||||||
"is_public_description": "Allow others to view this watchlist",
|
"is_public_description": "السماح للآخرين بعرض قائمة المشاهدة هذه",
|
||||||
"allowed_type_label": "Content Type",
|
"allowed_type_label": "نوع المحتوى",
|
||||||
"sort_order_label": "Default Sort Order",
|
"sort_order_label": "الترتيب الافتراضي",
|
||||||
"empty_title": "No Watchlists",
|
"empty_title": "لا توجد قوائم مشاهدة",
|
||||||
"empty_description": "Create your first watchlist to start organizing your media",
|
"empty_description": "قم بإنشاء أول قائمة مشاهدة لبدء تنظيم الوسائط الخاصة بك",
|
||||||
"empty_watchlist": "This watchlist is empty",
|
"empty_watchlist": "قائمة المشاهدة هذه فارغة",
|
||||||
"empty_watchlist_hint": "Add items from your library to this watchlist",
|
"empty_watchlist_hint": "إضافة عناصر من مكتبتك إلى قائمة المشاهدة هذه",
|
||||||
"not_configured_title": "Streamystats Not Configured",
|
"not_configured_title": "لم يتم ضبط Streamystats",
|
||||||
"not_configured_description": "Configure Streamystats in settings to use watchlists",
|
"not_configured_description": "اضبط Streamystats في الإعدادات لاستخدام قوائم المشاهدة",
|
||||||
"go_to_settings": "Go to Settings",
|
"go_to_settings": "الذهاب إلى الإعدادات",
|
||||||
"add_to_watchlist": "Add to Watchlist",
|
"add_to_watchlist": "إضافة إلى قائمة المشاهدة",
|
||||||
"remove_from_watchlist": "Remove from Watchlist",
|
"remove_from_watchlist": "إزالة من قائمة المشاهدة",
|
||||||
"select_watchlist": "Select Watchlist",
|
"select_watchlist": "تحديد قائمة المشاهدة",
|
||||||
"create_new": "Create New Watchlist",
|
"create_new": "إنشاء قائمة مشاهدة جديدة",
|
||||||
"item": "item",
|
"item": "عنصر",
|
||||||
"items": "items",
|
"items": "عناصر",
|
||||||
"public": "Public",
|
"public": "عامة",
|
||||||
"private": "Private",
|
"private": "خاصة",
|
||||||
"you": "You",
|
"you": "أنت",
|
||||||
"by_owner": "By another user",
|
"by_owner": "بواسطة مستخدم آخر",
|
||||||
"not_found": "Watchlist not found",
|
"not_found": "قائمة المشاهدة غير موجودة",
|
||||||
"delete_confirm_title": "Delete Watchlist",
|
"delete_confirm_title": "حذف قائمة المشاهدة",
|
||||||
"delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
"delete_confirm_message": "هل أنت متأكد من رغبتك في حذف \"{{name}}\"؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
"remove_item_title": "Remove from Watchlist",
|
"remove_item_title": "إزالة من قائمة المشاهدة",
|
||||||
"remove_item_message": "Remove \"{{name}}\" from this watchlist?",
|
"remove_item_message": "إزالة \"{{name}}\" من قائمة المشاهدة هذه؟",
|
||||||
"loading": "Loading watchlists...",
|
"loading": "تحميل قوائم المشاهدة...",
|
||||||
"no_compatible_watchlists": "No compatible watchlists",
|
"no_compatible_watchlists": "لا توجد قوائم مشاهدة متوافقة",
|
||||||
"create_one_first": "Create a watchlist that accepts this content type"
|
"create_one_first": "إنشاء قائمة مشاهدة تقبل نوع المحتوى هذا"
|
||||||
},
|
},
|
||||||
"playback_speed": {
|
"playback_speed": {
|
||||||
"title": "Playback Speed",
|
"title": "سرعة التشغيل",
|
||||||
"apply_to": "Apply To",
|
"apply_to": "تطبيق على",
|
||||||
"speed": "Speed",
|
"speed": "السرعة",
|
||||||
"scope": {
|
"scope": {
|
||||||
"media": "This media only",
|
"media": "الوسائط هذه فقط",
|
||||||
"show": "This show",
|
"show": "هذا المسلسل",
|
||||||
"all": "All media (default)"
|
"all": "جميع الوسائط (الافتراضي)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A continuació",
|
"next_up": "A continuació",
|
||||||
"no_items_to_display": "No hi ha elements per mostrar",
|
"no_items_to_display": "No hi ha elements per mostrar",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Další nahoru",
|
"next_up": "Další nahoru",
|
||||||
"no_items_to_display": "Žádné položky k zobrazení",
|
"no_items_to_display": "Žádné položky k zobrazení",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Næste",
|
"next_up": "Næste",
|
||||||
"no_items_to_display": "Ingen elementer at vise",
|
"no_items_to_display": "Ingen elementer at vise",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Nein",
|
"downloaded_file_no": "Nein",
|
||||||
"downloaded_file_cancel": "Abbrechen"
|
"downloaded_file_cancel": "Abbrechen"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Als Nächstes",
|
"next_up": "Als Nächstes",
|
||||||
"no_items_to_display": "Keine Elemente",
|
"no_items_to_display": "Keine Elemente",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Επόμενο Επάνω",
|
"next_up": "Επόμενο Επάνω",
|
||||||
"no_items_to_display": "Δεν υπάρχουν στοιχεία προς εμφάνιση",
|
"no_items_to_display": "Δεν υπάρχουν στοιχεία προς εμφάνιση",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"no_items_to_display": "No Items to Display",
|
"no_items_to_display": "No Items to Display",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancelar"
|
"downloaded_file_cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A continuación",
|
"next_up": "A continuación",
|
||||||
"no_items_to_display": "No hay ítems para mostrar",
|
"no_items_to_display": "No hay ítems para mostrar",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Ei",
|
"downloaded_file_no": "Ei",
|
||||||
"downloaded_file_cancel": "Peruuta"
|
"downloaded_file_cancel": "Peruuta"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Seuraavaksi",
|
"next_up": "Seuraavaksi",
|
||||||
"no_items_to_display": "Ei kohteita näytettäväksi",
|
"no_items_to_display": "Ei kohteita näytettäväksi",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Non",
|
"downloaded_file_no": "Non",
|
||||||
"downloaded_file_cancel": "Annuler"
|
"downloaded_file_cancel": "Annuler"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "À suivre",
|
"next_up": "À suivre",
|
||||||
"no_items_to_display": "Aucuns médias à afficher",
|
"no_items_to_display": "Aucuns médias à afficher",
|
||||||
@@ -788,8 +794,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watchlists": {
|
"watchlists": {
|
||||||
"title": "Watchlists",
|
"title": "Listes de lecture",
|
||||||
"my_watchlists": "My Watchlists",
|
"my_watchlists": "Mes listes de lecture",
|
||||||
"public_watchlists": "Watchlist publique",
|
"public_watchlists": "Watchlist publique",
|
||||||
"create_title": "Créer une Watchlist",
|
"create_title": "Créer une Watchlist",
|
||||||
"edit_title": "Modifier la Watchlist",
|
"edit_title": "Modifier la Watchlist",
|
||||||
@@ -802,7 +808,7 @@
|
|||||||
"name_placeholder": "Entrer le nom de la playlist",
|
"name_placeholder": "Entrer le nom de la playlist",
|
||||||
"description_label": "Description",
|
"description_label": "Description",
|
||||||
"description_placeholder": "Entrez la description (facultatif)",
|
"description_placeholder": "Entrez la description (facultatif)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "Liste de lecture Publique",
|
||||||
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
|
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
|
||||||
"allowed_type_label": "Type de contenu",
|
"allowed_type_label": "Type de contenu",
|
||||||
"sort_order_label": "Ordre de tri par défaut",
|
"sort_order_label": "Ordre de tri par défaut",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "Your saved session has expired. Please log in again.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "Remove Saved Login",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} חשבונות",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
||||||
@@ -110,16 +110,16 @@
|
|||||||
"settings_title": "הגדרות",
|
"settings_title": "הגדרות",
|
||||||
"log_out_button": "התנתק",
|
"log_out_button": "התנתק",
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "קטגוריות"
|
||||||
},
|
},
|
||||||
"playback_controls": {
|
"playback_controls": {
|
||||||
"title": "Playback & Controls"
|
"title": "Playback & Controls"
|
||||||
},
|
},
|
||||||
"audio_subtitles": {
|
"audio_subtitles": {
|
||||||
"title": "Audio & Subtitles"
|
"title": "שמע וכתוביות"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "מראה",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button"
|
||||||
},
|
},
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "אודיו",
|
"audio_title": "שמע",
|
||||||
"set_audio_track": "בחר רצועת שמע מהפריט הקודם",
|
"set_audio_track": "בחר רצועת שמע מהפריט הקודם",
|
||||||
"audio_language": "שפת שמע",
|
"audio_language": "שפת שמע",
|
||||||
"audio_hint": "בחר שפת שמע אוטומטית.",
|
"audio_hint": "בחר שפת שמע אוטומטית.",
|
||||||
@@ -271,8 +271,8 @@
|
|||||||
"margin": "Bottom Margin"
|
"margin": "Bottom Margin"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "נגן וידאו",
|
||||||
"video_player": "Video Player",
|
"video_player": "נגן וידאו",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "Choose which video player to use on iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
"downloads_title": "הורדות"
|
"downloads_title": "הורדות"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "מוזיקה",
|
||||||
"playback_title": "Playback",
|
"playback_title": "Playback",
|
||||||
"playback_description": "Configure how music is played.",
|
"playback_description": "Configure how music is played.",
|
||||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
"prefer_downloaded": "Prefer Downloaded Songs",
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "הקדמה",
|
||||||
"show_intro": "הצג פתיח",
|
"show_intro": "הצג פתיח",
|
||||||
"reset_intro": "אפס פתיח"
|
"reset_intro": "אפס פתיח"
|
||||||
},
|
},
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
"select": "בחר",
|
"select": "בחר",
|
||||||
"no_trailer_available": "אין טריילר זמין",
|
"no_trailer_available": "אין טריילר זמין",
|
||||||
"video": "וידאו",
|
"video": "וידאו",
|
||||||
"audio": "אודיו",
|
"audio": "שמע",
|
||||||
"subtitle": "כתובית",
|
"subtitle": "כתובית",
|
||||||
"play": "נגן",
|
"play": "נגן",
|
||||||
"none": "ללא",
|
"none": "ללא",
|
||||||
@@ -521,9 +521,9 @@
|
|||||||
"episodes": "פרקים",
|
"episodes": "פרקים",
|
||||||
"collections": "אוספים",
|
"collections": "אוספים",
|
||||||
"actors": "שחקנים",
|
"actors": "שחקנים",
|
||||||
"artists": "Artists",
|
"artists": "אומנים",
|
||||||
"albums": "Albums",
|
"albums": "אלבומים",
|
||||||
"songs": "Songs",
|
"songs": "שירים",
|
||||||
"playlists": "Playlists",
|
"playlists": "Playlists",
|
||||||
"request_movies": "סרטים מבוקשים",
|
"request_movies": "סרטים מבוקשים",
|
||||||
"request_series": "סדרות מבוקשים",
|
"request_series": "סדרות מבוקשים",
|
||||||
@@ -606,10 +606,16 @@
|
|||||||
"go_back": "חזור",
|
"go_back": "חזור",
|
||||||
"downloaded_file_title": "You have this file downloaded",
|
"downloaded_file_title": "You have this file downloaded",
|
||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "כן",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "לא",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "הבא בתור",
|
"next_up": "הבא בתור",
|
||||||
"no_items_to_display": "אין פריטים להציג",
|
"no_items_to_display": "אין פריטים להציג",
|
||||||
@@ -626,7 +632,7 @@
|
|||||||
"more_details": "פרטים נוספים",
|
"more_details": "פרטים נוספים",
|
||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "איכות",
|
"quality": "איכות",
|
||||||
"audio": "אודיו",
|
"audio": "שמע",
|
||||||
"subtitles": "כתובית",
|
"subtitles": "כתובית",
|
||||||
"show_more": "הצג עוד",
|
"show_more": "הצג עוד",
|
||||||
"show_less": "הצג פחות",
|
"show_less": "הצג פחות",
|
||||||
@@ -719,10 +725,10 @@
|
|||||||
"favorites": "מועדפים"
|
"favorites": "מועדפים"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "מוזיקה",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"albums": "Albums",
|
"albums": "אלבומים",
|
||||||
"artists": "Artists",
|
"artists": "Artists",
|
||||||
"playlists": "Playlists",
|
"playlists": "Playlists",
|
||||||
"tracks": "tracks"
|
"tracks": "tracks"
|
||||||
@@ -798,9 +804,9 @@
|
|||||||
"delete_button": "Delete",
|
"delete_button": "Delete",
|
||||||
"remove_button": "Remove",
|
"remove_button": "Remove",
|
||||||
"cancel_button": "Cancel",
|
"cancel_button": "Cancel",
|
||||||
"name_label": "Name",
|
"name_label": "שם",
|
||||||
"name_placeholder": "Enter watchlist name",
|
"name_placeholder": "Enter watchlist name",
|
||||||
"description_label": "Description",
|
"description_label": "תיאור",
|
||||||
"description_placeholder": "Enter description (optional)",
|
"description_placeholder": "Enter description (optional)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "Public Watchlist",
|
||||||
"is_public_description": "Allow others to view this watchlist",
|
"is_public_description": "Allow others to view this watchlist",
|
||||||
@@ -817,10 +823,10 @@
|
|||||||
"remove_from_watchlist": "Remove from Watchlist",
|
"remove_from_watchlist": "Remove from Watchlist",
|
||||||
"select_watchlist": "Select Watchlist",
|
"select_watchlist": "Select Watchlist",
|
||||||
"create_new": "Create New Watchlist",
|
"create_new": "Create New Watchlist",
|
||||||
"item": "item",
|
"item": "פריט",
|
||||||
"items": "items",
|
"items": "פריטים",
|
||||||
"public": "Public",
|
"public": "ציבורי",
|
||||||
"private": "Private",
|
"private": "פרטי",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"by_owner": "By another user",
|
"by_owner": "By another user",
|
||||||
"not_found": "Watchlist not found",
|
"not_found": "Watchlist not found",
|
||||||
@@ -835,7 +841,7 @@
|
|||||||
"playback_speed": {
|
"playback_speed": {
|
||||||
"title": "Playback Speed",
|
"title": "Playback Speed",
|
||||||
"apply_to": "Apply To",
|
"apply_to": "Apply To",
|
||||||
"speed": "Speed",
|
"speed": "מהירות",
|
||||||
"scope": {
|
"scope": {
|
||||||
"media": "This media only",
|
"media": "This media only",
|
||||||
"show": "This show",
|
"show": "This show",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Következő",
|
"next_up": "Következő",
|
||||||
"no_items_to_display": "Nincs Megjeleníthető Elem",
|
"no_items_to_display": "Nincs Megjeleníthető Elem",
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "Not connected to WiFi",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "No networks configured",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "WiFi Attuale",
|
||||||
"using_url": "Sta utilizzando",
|
"using_url": "Sta utilizzando",
|
||||||
"local": "Local URL",
|
"local": "Local URL",
|
||||||
"remote": "Remote URL",
|
"remote": "Remote URL",
|
||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Il prossimo",
|
"next_up": "Il prossimo",
|
||||||
"no_items_to_display": "Nessun elemento da visualizzare",
|
"no_items_to_display": "Nessun elemento da visualizzare",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "次",
|
"next_up": "次",
|
||||||
"no_items_to_display": "表示するアイテムがありません",
|
"no_items_to_display": "表示するアイテムがありません",
|
||||||
|
|||||||
@@ -1,187 +1,187 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"username_required": "Username Is Required",
|
"username_required": "사용자 이름이 필요합니다",
|
||||||
"error_title": "Error",
|
"error_title": "오류",
|
||||||
"login_title": "Log In",
|
"login_title": "로그인",
|
||||||
"login_to_title": "Log in to",
|
"login_to_title": "다음 서비스에 연결 중",
|
||||||
"username_placeholder": "Username",
|
"username_placeholder": "사용자 이름",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "비밀번호",
|
||||||
"login_button": "Log In",
|
"login_button": "로그인",
|
||||||
"quick_connect": "Quick Connect",
|
"quick_connect": "퀵 커넥트",
|
||||||
"enter_code_to_login": "Enter code {{code}} to login",
|
"enter_code_to_login": "로그인 하기 위해 코드{{code}}를 입력하세요",
|
||||||
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
"failed_to_initiate_quick_connect": "Quick Connect 연결을 시작하는 데 실패했습니다",
|
||||||
"got_it": "Got It",
|
"got_it": "성공",
|
||||||
"connection_failed": "Connection Failed",
|
"connection_failed": "연결 실패",
|
||||||
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
"could_not_connect_to_server": "서버에 연결되지 않았습니다. URL과 네트워크 상태를 확인하세요.",
|
||||||
"an_unexpected_error_occured": "An Unexpected Error Occurred",
|
"an_unexpected_error_occured": "예기치 않은 오류가 발생했습니다",
|
||||||
"change_server": "Change Server",
|
"change_server": "서버 변경",
|
||||||
"invalid_username_or_password": "Invalid Username or Password",
|
"invalid_username_or_password": "잘못된 아이디 혹은 비밀번호입니다",
|
||||||
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
"user_does_not_have_permission_to_log_in": "로그인 하기 위한 권한이 없습니다",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
"server_is_taking_too_long_to_respond_try_again_later": "서버 응답이 너무 느립니다. 나중에 다시 시도하세요",
|
||||||
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
"server_received_too_many_requests_try_again_later": "서버가 너무 많은 요청을 받았습니다. 나중에 다시 시도하세요.",
|
||||||
"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": "예기치 않은 오류가 발생했습니다. 서버 URL을 올바르게 입력하셨습니까?",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
"connect_button": "Connect",
|
"connect_button": "연결",
|
||||||
"previous_servers": "Previous Servers",
|
"previous_servers": "이전 서버",
|
||||||
"clear_button": "Clear all",
|
"clear_button": "모두 지우기",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "스와이프해서 지우기",
|
||||||
"search_for_local_servers": "Search for Local Servers",
|
"search_for_local_servers": "로컬 서버 찾기",
|
||||||
"searching": "Searching...",
|
"searching": "찾는 중...",
|
||||||
"servers": "Servers",
|
"servers": "서버",
|
||||||
"saved": "Saved",
|
"saved": "저장됨",
|
||||||
"session_expired": "Session Expired",
|
"session_expired": "세션 만료됨",
|
||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "사용자 세션이 만료되었습니다. 다시 로그인하십시오.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "저장된 로그인 정보 삭제",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "해당 서버에 저장된 자격 증명이 삭제됩니다. 다음에 접속할 때는 사용자 이름과 비밀번호를 다시 입력해야 합니다.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} 계정",
|
||||||
"select_account": "Select Account",
|
"select_account": "계정 선택",
|
||||||
"add_account": "Add Account",
|
"add_account": "계정 추가",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "{{username}}에 저장된 자격 증명이 삭제됩니다."
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "계정 저장",
|
||||||
"save_for_later": "Save this account",
|
"save_for_later": "이 계정 저장",
|
||||||
"security_option": "Security Option",
|
"security_option": "보안 설정",
|
||||||
"no_protection": "No protection",
|
"no_protection": "보안 없음",
|
||||||
"no_protection_desc": "Quick login without authentication",
|
"no_protection_desc": "인증 없이 빠른 로그인",
|
||||||
"pin_code": "PIN code",
|
"pin_code": "PIN 코드",
|
||||||
"pin_code_desc": "4-digit PIN required when switching",
|
"pin_code_desc": "전환하려면 4자리 PIN 필요함",
|
||||||
"password": "Re-enter password",
|
"password": "암호 확인",
|
||||||
"password_desc": "Password required when switching",
|
"password_desc": "전환하려면 비밀번호 필요함",
|
||||||
"save_button": "Save",
|
"save_button": "저장",
|
||||||
"cancel_button": "Cancel"
|
"cancel_button": "취소"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "PIN 입력",
|
||||||
"enter_pin_for": "Enter PIN for {{username}}",
|
"enter_pin_for": "{{username}} PIN 입력",
|
||||||
"enter_4_digits": "Enter 4 digits",
|
"enter_4_digits": "4자리 입력",
|
||||||
"invalid_pin": "Invalid PIN",
|
"invalid_pin": "잘못된 PIN",
|
||||||
"setup_pin": "Set Up PIN",
|
"setup_pin": "PIN 설정",
|
||||||
"confirm_pin": "Confirm PIN",
|
"confirm_pin": "PIN 확인",
|
||||||
"pins_dont_match": "PINs don't match",
|
"pins_dont_match": "PIN이 일치하지 않습니다",
|
||||||
"forgot_pin": "Forgot PIN?",
|
"forgot_pin": "PIN을 잊으셨나요?",
|
||||||
"forgot_pin_desc": "Your saved credentials will be removed"
|
"forgot_pin_desc": "저장된 계정 정보가 삭제됩니다"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"enter_password": "Enter Password",
|
"enter_password": "비밀번호 입력",
|
||||||
"enter_password_for": "Enter password for {{username}}",
|
"enter_password_for": "{{username}}의 비밀번호 입력",
|
||||||
"invalid_password": "Invalid password"
|
"invalid_password": "잘못된 비밀번호"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"checking_server_connection": "Checking server connection...",
|
"checking_server_connection": "서버 연결 체크중...",
|
||||||
"no_internet": "No Internet",
|
"no_internet": "인터넷에 연결되지 않음",
|
||||||
"no_items": "No Items",
|
"no_items": "항목 없음",
|
||||||
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
"no_internet_message": "걱정마세요. 다운로드 된 컨텐츠는 여전히 볼 수 있습니다.",
|
||||||
"checking_server_connection_message": "Checking connection to server",
|
"checking_server_connection_message": "Checking connection to server",
|
||||||
"go_to_downloads": "Go to Downloads",
|
"go_to_downloads": "Go to Downloads",
|
||||||
"retry": "Retry",
|
"retry": "재시도",
|
||||||
"server_unreachable": "Server Unreachable",
|
"server_unreachable": "서버에 연결할 수 없음",
|
||||||
"server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
|
"server_unreachable_message": "서버에 연결할 수 없습니다. 네트워크 상태를 체크하세요.",
|
||||||
"oops": "Oops!",
|
"oops": "이런!",
|
||||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
"error_message": "문제가 발생했습니다.\n로그아웃 후 다시 로그인해 주세요.",
|
||||||
"continue_watching": "Continue Watching",
|
"continue_watching": "이어서 보기",
|
||||||
"next_up": "Next Up",
|
"next_up": "다음 시청",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "이어서 보기 & 다음 시청",
|
||||||
"recently_added_in": "Recently Added in {{libraryName}}",
|
"recently_added_in": "최근에 추가된 {{libraryName}}",
|
||||||
"suggested_movies": "Suggested Movies",
|
"suggested_movies": "추천 영화",
|
||||||
"suggested_episodes": "Suggested Episodes",
|
"suggested_episodes": "추천 에피소드",
|
||||||
"intro": {
|
"intro": {
|
||||||
"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": "젤리핀을 위한 무료 오픈소스 클라이언트입니다.",
|
||||||
"features_title": "Features",
|
"features_title": "기능",
|
||||||
"features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:",
|
"features_description": "스트리미핀은 다양한 기능을 제공하며 설정 메뉴에서 확인할 수 있는 여러 소프트웨어와 통합됩니다. 이러한 소프트웨어에는 다음이 포함됩니다:",
|
||||||
"jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
|
"jellyseerr_feature_description": "Seerr 인스턴스에 연결하여 앱에서 직접 영화를 요청할 수 있습니다.",
|
||||||
"downloads_feature_title": "Downloads",
|
"downloads_feature_title": "다운로드된 컨텐츠",
|
||||||
"downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.",
|
"downloads_feature_description": "오프라인으로 보기위해 다운로드 하세요. 기본 다운로드 방식을 사용하거나, 백그라운드에서 파일을 다운로드하는 최적화 서버를 설치할 수 있습니다.",
|
||||||
"chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.",
|
"chromecast_feature_description": "영화와 TV 프로그램을 Chromecast 기기로 전송하기",
|
||||||
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
"centralised_settings_plugin_title": "중앙 설정 플러그인",
|
||||||
"centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.",
|
"centralised_settings_plugin_description": "Jellyfin 서버의 중앙 집중식 위치에서 설정을 구성합니다. 모든 사용자의 모든 클라이언트 설정이 자동으로 동기화됩니다.",
|
||||||
"done_button": "Done",
|
"done_button": "확인",
|
||||||
"go_to_settings_button": "Go to Settings",
|
"go_to_settings_button": "설정으로 이동",
|
||||||
"read_more": "Read More"
|
"read_more": "자세히 보기"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "설정",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "로그아웃",
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "카테고리"
|
||||||
},
|
},
|
||||||
"playback_controls": {
|
"playback_controls": {
|
||||||
"title": "Playback & Controls"
|
"title": "재생 & 컨트롤"
|
||||||
},
|
},
|
||||||
"audio_subtitles": {
|
"audio_subtitles": {
|
||||||
"title": "Audio & Subtitles"
|
"title": "오디오 & 자막"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "화면 스타일",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "[이어보기]와 [다음 보기] 합치기",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "원격 세션 버튼 숨기기"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "네트워크",
|
||||||
"local_network": "Local Network",
|
"local_network": "로컬 네트워크",
|
||||||
"auto_switch_enabled": "Auto-switch when at home",
|
"auto_switch_enabled": "홈 네트워크 자동 전환",
|
||||||
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
|
"auto_switch_description": "홈 WiFi에 연결되었을 때 로컬 URL로 자동 전환",
|
||||||
"local_url": "Local URL",
|
"local_url": "로컬 URL",
|
||||||
"local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "로컬 서버 주소를 입력하세요 (e.g., http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Home WiFi Networks",
|
"home_wifi_networks": "홈 WiFi 네트워크",
|
||||||
"add_current_network": "Add \"{{ssid}}\"",
|
"add_current_network": "\"{{ssid}}\" 추가",
|
||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "WiFi에 연결되지 않음",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "구성된 네트워크가 없습니다",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "자동 전환을 위한 홈 WiFi 추가",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "현재 WiFi",
|
||||||
"using_url": "Using",
|
"using_url": "사용중",
|
||||||
"local": "Local URL",
|
"local": "로컬 URL",
|
||||||
"remote": "Remote URL",
|
"remote": "원격 URL",
|
||||||
"not_connected": "Not connected",
|
"not_connected": "연결되지 않았습니다",
|
||||||
"current_server": "Current Server",
|
"current_server": "현재 서버",
|
||||||
"remote_url": "Remote URL",
|
"remote_url": "원격 URL",
|
||||||
"active_url": "Active URL",
|
"active_url": "현재 사용 중인 URL",
|
||||||
"not_configured": "Not configured",
|
"not_configured": "설정되지 않음",
|
||||||
"network_added": "Network added",
|
"network_added": "네트워크 추가됨",
|
||||||
"network_already_added": "Network already added",
|
"network_already_added": "네트워크 이미 추가됨",
|
||||||
"no_wifi_connected": "Not connected to WiFi",
|
"no_wifi_connected": "WiFi에 연결되지 않음",
|
||||||
"permission_denied": "Location permission denied",
|
"permission_denied": "위치 권한이 거부되었습니다",
|
||||||
"permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
|
"permission_denied_explanation": "자동 전환 Wi-Fi 네트워크를 감지하려면 위치 권한이 필요합니다. 설정에서 위치 권한을 활성화해 주세요."
|
||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "User Info",
|
"user_info_title": "사용자 정보",
|
||||||
"user": "User",
|
"user": "사용자",
|
||||||
"server": "Server",
|
"server": "서버",
|
||||||
"token": "Token",
|
"token": "토큰",
|
||||||
"app_version": "App Version"
|
"app_version": "앱 버전"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"quick_connect": {
|
||||||
"quick_connect_title": "Quick Connect",
|
"quick_connect_title": "퀵 커넥트",
|
||||||
"authorize_button": "Authorize Quick Connect",
|
"authorize_button": "퀵 커넥트 승인",
|
||||||
"enter_the_quick_connect_code": "Enter the quick connect code...",
|
"enter_the_quick_connect_code": "퀵 커넥트 코드 입력...",
|
||||||
"success": "Success",
|
"success": "성공",
|
||||||
"quick_connect_autorized": "Quick Connect Authorized",
|
"quick_connect_autorized": "퀵 커넥트 승인됨",
|
||||||
"error": "Error",
|
"error": "오류",
|
||||||
"invalid_code": "Invalid Code",
|
"invalid_code": "유효하지 않은 코드",
|
||||||
"authorize": "Authorize"
|
"authorize": "승인"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Media Controls",
|
"media_controls_title": "미디어 컨트롤",
|
||||||
"forward_skip_length": "Forward Skip Length",
|
"forward_skip_length": "앞으로 건너뛸 시간",
|
||||||
"rewind_length": "Rewind Length",
|
"rewind_length": "뒤로 되감을 시간",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "초"
|
||||||
},
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gesture Controls",
|
"gesture_controls_title": "제스처 제어",
|
||||||
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
"horizontal_swipe_skip": "좌/우로 스와이프하여 건너뛰기",
|
||||||
"horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip",
|
"horizontal_swipe_skip_description": "컨트롤 숨김상태에서 좌/우로 스와이프하여 건너뛰기",
|
||||||
"left_side_brightness": "Left Side Brightness Control",
|
"left_side_brightness": "왼쪽 영역 밝기 조정 컨트롤",
|
||||||
"left_side_brightness_description": "Swipe up/down on left side to adjust brightness",
|
"left_side_brightness_description": "왼쪽 영역을 위/아래 스와이프하여 밝기 조절",
|
||||||
"right_side_volume": "Right Side Volume Control",
|
"right_side_volume": "오른쪽 영역 볼륨 컨트롤",
|
||||||
"right_side_volume_description": "Swipe up/down on right side to adjust volume",
|
"right_side_volume_description": "오른족 영역을 위/아래로 스와이프 하여 볼륨 조절",
|
||||||
"hide_volume_slider": "Hide Volume Slider",
|
"hide_volume_slider": "Hide Volume Slider",
|
||||||
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
||||||
"hide_brightness_slider": "Hide Brightness Slider",
|
"hide_brightness_slider": "Hide Brightness Slider",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"transcode_mode": {
|
"transcode_mode": {
|
||||||
"title": "Audio Transcoding",
|
"title": "Audio Transcoding",
|
||||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
"description": "서라운드 오디오(7.1, TrueHD, DTS-HD)를 어떻게 처리할지 설정합니다",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"stereo": "Force Stereo",
|
"stereo": "Force Stereo",
|
||||||
"5_1": "Allow 5.1",
|
"5_1": "Allow 5.1",
|
||||||
@@ -228,52 +228,52 @@
|
|||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "Outline Opacity",
|
||||||
"bold_text": "Bold Text",
|
"bold_text": "Bold Text",
|
||||||
"colors": {
|
"colors": {
|
||||||
"Black": "Black",
|
"Black": "검정색",
|
||||||
"Gray": "Gray",
|
"Gray": "회색",
|
||||||
"Silver": "Silver",
|
"Silver": "은색",
|
||||||
"White": "White",
|
"White": "흰색",
|
||||||
"Maroon": "Maroon",
|
"Maroon": "밤색",
|
||||||
"Red": "Red",
|
"Red": "빨간색",
|
||||||
"Fuchsia": "Fuchsia",
|
"Fuchsia": "분홍색",
|
||||||
"Yellow": "Yellow",
|
"Yellow": "노란색",
|
||||||
"Olive": "Olive",
|
"Olive": "올리브 색",
|
||||||
"Green": "Green",
|
"Green": "녹색",
|
||||||
"Teal": "Teal",
|
"Teal": "청록색",
|
||||||
"Lime": "Lime",
|
"Lime": "라임색",
|
||||||
"Purple": "Purple",
|
"Purple": "보라색",
|
||||||
"Navy": "Navy",
|
"Navy": "남색",
|
||||||
"Blue": "Blue",
|
"Blue": "파란색",
|
||||||
"Aqua": "Aqua"
|
"Aqua": "아쿠아색"
|
||||||
},
|
},
|
||||||
"thickness": {
|
"thickness": {
|
||||||
"None": "None",
|
"None": "없음",
|
||||||
"Thin": "Thin",
|
"Thin": "얇게",
|
||||||
"Normal": "Normal",
|
"Normal": "보통",
|
||||||
"Thick": "Thick"
|
"Thick": "굵게"
|
||||||
},
|
},
|
||||||
"subtitle_color": "Subtitle Color",
|
"subtitle_color": "자막 색상",
|
||||||
"subtitle_background_color": "Background Color",
|
"subtitle_background_color": "배경 색상",
|
||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "자막 폰트",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer 설정",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "하드웨어 디코딩",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "비디오 디코딩에 하드웨어 가속을 사용하십시오. 재생 문제가 발생하는 경우 비활성화하십시오."
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC 자막 설정",
|
||||||
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
|
"hint": "VLC 플레이어의 자막 표시 방식을 설정하세요. 변경 사항은 다음 재생 시 적용됩니다.",
|
||||||
"text_color": "Text Color",
|
"text_color": "글자색",
|
||||||
"background_color": "Background Color",
|
"background_color": "배경 색상",
|
||||||
"background_opacity": "Background Opacity",
|
"background_opacity": "배경 투명도",
|
||||||
"outline_color": "Outline Color",
|
"outline_color": "외곽선 색상",
|
||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "외곽선 투명도",
|
||||||
"outline_thickness": "Outline Thickness",
|
"outline_thickness": "외곽선 굵기",
|
||||||
"bold": "Bold Text",
|
"bold": "굵은 글씨",
|
||||||
"margin": "Bottom Margin"
|
"margin": "아래쪽 여백"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "비디오 플레이어",
|
||||||
"video_player": "Video Player",
|
"video_player": "비디오 플레이어",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "iOS 사용자는 비디오 플레이어를 선택하세요.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -288,20 +288,20 @@
|
|||||||
"PORTRAIT_UP": "Portrait Up",
|
"PORTRAIT_UP": "Portrait Up",
|
||||||
"PORTRAIT_DOWN": "Portrait Down",
|
"PORTRAIT_DOWN": "Portrait Down",
|
||||||
"LANDSCAPE": "Landscape",
|
"LANDSCAPE": "Landscape",
|
||||||
"LANDSCAPE_LEFT": "Landscape Left",
|
"LANDSCAPE_LEFT": "왼쪽 가로 모드",
|
||||||
"LANDSCAPE_RIGHT": "Landscape Right",
|
"LANDSCAPE_RIGHT": "오른쪽 가로 모드",
|
||||||
"OTHER": "Other",
|
"OTHER": "Other",
|
||||||
"UNKNOWN": "Unknown"
|
"UNKNOWN": "Unknown"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe Area in Controls",
|
"safe_area_in_controls": "컨트롤 안전 영역",
|
||||||
"video_player": "Video Player",
|
"video_player": "Video Player",
|
||||||
"video_players": {
|
"video_players": {
|
||||||
"VLC_3": "VLC 3",
|
"VLC_3": "VLC 3",
|
||||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
},
|
},
|
||||||
"show_custom_menu_links": "Show Custom Menu Links",
|
"show_custom_menu_links": "사용자 지정 메뉴 링크 표시",
|
||||||
"show_large_home_carousel": "Show Large Home Carousel (beta)",
|
"show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)",
|
||||||
"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",
|
||||||
"default_quality": "Default Quality",
|
"default_quality": "Default Quality",
|
||||||
@@ -334,24 +334,24 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password_placeholder": "Enter password for Jellyfin user {{username}}",
|
"password_placeholder": "Enter password for Jellyfin user {{username}}",
|
||||||
"login_button": "Login",
|
"login_button": "Login",
|
||||||
"total_media_requests": "Total Media Requests",
|
"total_media_requests": "전체 미디어 요청 수",
|
||||||
"movie_quota_limit": "Movie Quota Limit",
|
"movie_quota_limit": "영화 요청 한도",
|
||||||
"movie_quota_days": "Movie Quota Days",
|
"movie_quota_days": "영화 요청 제한 기간",
|
||||||
"tv_quota_limit": "TV Quota Limit",
|
"tv_quota_limit": "TV Quota Limit",
|
||||||
"tv_quota_days": "TV Quota Days",
|
"tv_quota_days": "TV 요청 제한 기간",
|
||||||
"reset_jellyseerr_config_button": "Reset Seerr Config",
|
"reset_jellyseerr_config_button": "Seerr 설정 초기화",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"plus_n_more": "+{{n}} More",
|
"plus_n_more": "+{{n}}개 더",
|
||||||
"order_by": {
|
"order_by": {
|
||||||
"DEFAULT": "Default",
|
"DEFAULT": "Default",
|
||||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
"VOTE_COUNT_AND_AVERAGE": "평균 평점 및 투표 수",
|
||||||
"POPULARITY": "Popularity"
|
"POPULARITY": "Popularity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Enable Marlin Search",
|
"enable_marlin_search": "Marlin 검색 활성화",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"server_url_placeholder": "http(s)://도메인:포트",
|
||||||
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
||||||
"read_more_about_marlin": "Read More About Marlin.",
|
"read_more_about_marlin": "Read More About Marlin.",
|
||||||
"save_button": "Save",
|
"save_button": "Save",
|
||||||
@@ -374,28 +374,28 @@
|
|||||||
"features_title": "Features",
|
"features_title": "Features",
|
||||||
"home_sections_title": "Home Sections",
|
"home_sections_title": "Home Sections",
|
||||||
"enable_movie_recommendations": "Movie Recommendations",
|
"enable_movie_recommendations": "Movie Recommendations",
|
||||||
"enable_series_recommendations": "Series Recommendations",
|
"enable_series_recommendations": "시리즈 추천",
|
||||||
"enable_promoted_watchlists": "Promoted Watchlists",
|
"enable_promoted_watchlists": "추천 관심 목록",
|
||||||
"hide_watchlists_tab": "Hide Watchlists Tab",
|
"hide_watchlists_tab": "관심 목록 탭 숨기기",
|
||||||
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
|
"home_sections_hint": "홈 페이지에서 Streamystats의 개인 맞춤 추천 및 추천 관심 목록을 표시합니다.",
|
||||||
"recommended_movies": "Recommended Movies",
|
"recommended_movies": "Recommended Movies",
|
||||||
"recommended_series": "Recommended Series",
|
"recommended_series": "추천 시리즈",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"refreshed": "Settings refreshed from server",
|
"refreshed": "서버에서 설정을 새로고침했습니다",
|
||||||
"disabled": "Streamystats disabled"
|
"disabled": "Streamystats 비활성화됨"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "서버에서 설정 새로고침"
|
||||||
},
|
},
|
||||||
"kefinTweaks": {
|
"kefinTweaks": {
|
||||||
"watchlist_enabler": "Enable our Watchlist integration",
|
"watchlist_enabler": "관심 목록 통합 기능 활성화",
|
||||||
"watchlist_button": "Toggle Watchlist integration"
|
"watchlist_button": "관심 목록 연동 켜기/끄기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"storage_title": "Storage",
|
"storage_title": "Storage",
|
||||||
"app_usage": "App {{usedSpace}}%",
|
"app_usage": "앱 {{usedSpace}}",
|
||||||
"device_usage": "Device {{availableSpace}}%",
|
"device_usage": "디바이스 {{availableSpace}}%",
|
||||||
"size_used": "{{used}} of {{total}} Used",
|
"size_used": "{{used}} of {{total}} Used",
|
||||||
"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",
|
||||||
@@ -403,10 +403,10 @@
|
|||||||
"enable_music_cache": "Enable Music Cache",
|
"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": "음악 캐시가 삭제되었습니다",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "다운로드한 노래가 삭제되었습니다"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessions",
|
"title": "Sessions",
|
||||||
"no_active_sessions": "No Active Sessions"
|
"no_active_sessions": "세션 비활성화"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"no_items_to_display": "No Items to Display",
|
"no_items_to_display": "No Items to Display",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Nee",
|
"downloaded_file_no": "Nee",
|
||||||
"downloaded_file_cancel": "Annuleren"
|
"downloaded_file_cancel": "Annuleren"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Volgende",
|
"next_up": "Volgende",
|
||||||
"no_items_to_display": "Geen items om te tonen",
|
"no_items_to_display": "Geen items om te tonen",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Neste opp",
|
"next_up": "Neste opp",
|
||||||
"no_items_to_display": "Ingen elementer å vise",
|
"no_items_to_display": "Ingen elementer å vise",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Nie",
|
"downloaded_file_no": "Nie",
|
||||||
"downloaded_file_cancel": "Anuluj"
|
"downloaded_file_cancel": "Anuluj"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Następne",
|
"next_up": "Następne",
|
||||||
"no_items_to_display": "Brak elementów do wyświetlenia",
|
"no_items_to_display": "Brak elementów do wyświetlenia",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Não",
|
"downloaded_file_no": "Não",
|
||||||
"downloaded_file_cancel": "Cancelar"
|
"downloaded_file_cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A Seguir",
|
"next_up": "A Seguir",
|
||||||
"no_items_to_display": "Nenhum item para exibir",
|
"no_items_to_display": "Nenhum item para exibir",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Nu",
|
"downloaded_file_no": "Nu",
|
||||||
"downloaded_file_cancel": "Anulează"
|
"downloaded_file_cancel": "Anulează"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Urmează",
|
"next_up": "Urmează",
|
||||||
"no_items_to_display": "Niciun element de afișat",
|
"no_items_to_display": "Niciun element de afișat",
|
||||||
|
|||||||
@@ -12,25 +12,25 @@
|
|||||||
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
||||||
"got_it": "Принято",
|
"got_it": "Принято",
|
||||||
"connection_failed": "Соединение не удалось",
|
"connection_failed": "Соединение не удалось",
|
||||||
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
|
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста, проверьте URL и ваше интернет-соединение.",
|
||||||
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
||||||
"change_server": "Поменять сервер",
|
"change_server": "Поменять сервер",
|
||||||
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
||||||
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
|
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже",
|
||||||
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
||||||
"there_is_a_server_error": "Возникла ошибка сервера",
|
"there_is_a_server_error": "Возникла ошибка сервера",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?",
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?",
|
||||||
"too_old_server_text": "Неподдерживаемый сервер Jellyfin обнаружен",
|
"too_old_server_text": "Обнаружен неподдерживаемый сервер Jellyfin",
|
||||||
"too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии"
|
"too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
"connect_button": "Подключиться",
|
"connect_button": "Подключиться",
|
||||||
"previous_servers": "предыдущие серверы",
|
"previous_servers": "Предыдущие серверы",
|
||||||
"clear_button": "Очистить",
|
"clear_button": "Очистить",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "Смахните для удаления",
|
||||||
"search_for_local_servers": "Поиск локальных серверов",
|
"search_for_local_servers": "Поиск локальных серверов",
|
||||||
"searching": "Поиск...",
|
"searching": "Поиск...",
|
||||||
"servers": "Сервера",
|
"servers": "Сервера",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.",
|
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.",
|
||||||
"remove_saved_login": "Удалить сохраненный аккаунт",
|
"remove_saved_login": "Удалить сохраненный аккаунт",
|
||||||
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.",
|
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.",
|
||||||
"accounts_count": "{{count}} аккаунтов",
|
"accounts_count": "Аккаунтов: {{count}}",
|
||||||
"select_account": "Выбрать аккаунт",
|
"select_account": "Выбрать аккаунт",
|
||||||
"add_account": "Добавить аккаунт",
|
"add_account": "Добавить аккаунт",
|
||||||
"remove_account_description": "Данные для входа {{username}} будут удалены."
|
"remove_account_description": "Данные для входа {{username}} будут удалены."
|
||||||
@@ -58,14 +58,14 @@
|
|||||||
"cancel_button": "Отмена"
|
"cancel_button": "Отмена"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Введите PIN",
|
"enter_pin": "Введите PIN-код",
|
||||||
"enter_pin_for": "Введите PIN для {{username}}",
|
"enter_pin_for": "Введите PIN-код для {{username}}",
|
||||||
"enter_4_digits": "Введите 4 цифры",
|
"enter_4_digits": "Введите 4 цифры",
|
||||||
"invalid_pin": "Некорректный PIN",
|
"invalid_pin": "Некорректный PIN-код",
|
||||||
"setup_pin": "Установить PIN",
|
"setup_pin": "Установить PIN-код",
|
||||||
"confirm_pin": "Подтвердите PIN",
|
"confirm_pin": "Подтвердите PIN-код",
|
||||||
"pins_dont_match": "PIN-коды не совпадают",
|
"pins_dont_match": "PIN-коды не совпадают",
|
||||||
"forgot_pin": "Забыли PIN?",
|
"forgot_pin": "Забыли PIN-код?",
|
||||||
"forgot_pin_desc": "Ваши данные для входа будут удалены"
|
"forgot_pin_desc": "Ваши данные для входа будут удалены"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
"server_unreachable": "Сервер недоступен",
|
"server_unreachable": "Сервер недоступен",
|
||||||
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.",
|
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.",
|
||||||
"oops": "Упс!",
|
"oops": "Упс!",
|
||||||
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
|
"error_message": "Что-то пошло не так.\nПожалуйста, выйдите и зайдите снова.",
|
||||||
"continue_watching": "Продолжить",
|
"continue_watching": "Продолжить",
|
||||||
"next_up": "Далее",
|
"next_up": "Далее",
|
||||||
"continue_and_next_up": "Продолжить и Далее",
|
"continue_and_next_up": "Продолжить и Далее",
|
||||||
@@ -93,13 +93,13 @@
|
|||||||
"suggested_episodes": "Предложенные серии",
|
"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 с открытым кодом.",
|
||||||
"features_title": "Функции",
|
"features_title": "Функции",
|
||||||
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
||||||
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
||||||
"downloads_feature_title": "Загрузки",
|
"downloads_feature_title": "Загрузки",
|
||||||
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
||||||
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
|
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройства с поддержкой Chromecast.",
|
||||||
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
||||||
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
||||||
"done_button": "Готово",
|
"done_button": "Готово",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"auto_switch_enabled": "Переключаться дома автоматически",
|
"auto_switch_enabled": "Переключаться дома автоматически",
|
||||||
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети",
|
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети",
|
||||||
"local_url": "Локальный URL",
|
"local_url": "Локальный URL",
|
||||||
"local_url_hint": "Введите локальный URL вашего сервера (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "Введите локальный URL вашего сервера (например, http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Домашние WiFi сети",
|
"home_wifi_networks": "Домашние WiFi сети",
|
||||||
"add_current_network": "Добавить \"{{ssid}}\"",
|
"add_current_network": "Добавить \"{{ssid}}\"",
|
||||||
@@ -160,28 +160,28 @@
|
|||||||
},
|
},
|
||||||
"quick_connect": {
|
"quick_connect": {
|
||||||
"quick_connect_title": "Быстрое подключение",
|
"quick_connect_title": "Быстрое подключение",
|
||||||
"authorize_button": "Авторизировать через быстрое подключение",
|
"authorize_button": "Авторизовать через быстрое подключение",
|
||||||
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
||||||
"success": "Успех",
|
"success": "Успех",
|
||||||
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"invalid_code": "Неверный код",
|
"invalid_code": "Неверный код",
|
||||||
"authorize": "Авторизировать"
|
"authorize": "Авторизовать"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Медиа-контроль",
|
"media_controls_title": "Управление воспроизведением",
|
||||||
"forward_skip_length": "Шаг перемотки вперёд",
|
"forward_skip_length": "Шаг перемотки вперёд",
|
||||||
"rewind_length": "Шаг перемотки назад",
|
"rewind_length": "Шаг перемотки назад",
|
||||||
"seconds_unit": "c"
|
"seconds_unit": "c"
|
||||||
},
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Управление жестами",
|
"gesture_controls_title": "Управление жестами",
|
||||||
"horizontal_swipe_skip": "Горизонтальный свайп для перемотки",
|
"horizontal_swipe_skip": "Проведите влево/вправо для перемотки",
|
||||||
"horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы пропустить",
|
"horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы перемотать",
|
||||||
"left_side_brightness": "Управление яркостью левой стороны",
|
"left_side_brightness": "Управление яркостью слева",
|
||||||
"left_side_brightness_description": "Смахните вверх/вниз на левой стороне для настройки яркости",
|
"left_side_brightness_description": "Проведите вверх/вниз на левой стороне для настройки яркости",
|
||||||
"right_side_volume": "Управление громкостью справа",
|
"right_side_volume": "Управление громкостью справа",
|
||||||
"right_side_volume_description": "Свайп вверх/вниз с правой стороны для настройки громкости",
|
"right_side_volume_description": "Проведите вверх/вниз с правой стороны для настройки громкости",
|
||||||
"hide_volume_slider": "Скрыть индикатор громкости",
|
"hide_volume_slider": "Скрыть индикатор громкости",
|
||||||
"hide_volume_slider_description": "Скрывает индикатор громкости в плеере",
|
"hide_volume_slider_description": "Скрывает индикатор громкости в плеере",
|
||||||
"hide_brightness_slider": "Скрыть индикатор яркости",
|
"hide_brightness_slider": "Скрыть индикатор яркости",
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
},
|
},
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
"subtitle_title": "Субтитры",
|
"subtitle_title": "Субтитры",
|
||||||
"subtitle_hint": "Настройки отображения субтитров",
|
"subtitle_hint": "Настройки отображения субтитров.",
|
||||||
"subtitle_language": "Язык субтитров",
|
"subtitle_language": "Язык субтитров",
|
||||||
"subtitle_mode": "Режим субтитров",
|
"subtitle_mode": "Режим субтитров",
|
||||||
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
||||||
@@ -271,9 +271,9 @@
|
|||||||
"margin": "Отступ снизу"
|
"margin": "Отступ снизу"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Видеоплеер",
|
"title": "Видео плеер",
|
||||||
"video_player": "Видеоплеер",
|
"video_player": "Видео плеер",
|
||||||
"video_player_description": "Выберите видеоплеер в iOS.",
|
"video_player_description": "Выберите видео плеер в iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -294,12 +294,12 @@
|
|||||||
"UNKNOWN": "Неизвестное"
|
"UNKNOWN": "Неизвестное"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
||||||
"video_player": "Видеоплеер",
|
"video_player": "Видео плеер",
|
||||||
"video_players": {
|
"video_players": {
|
||||||
"VLC_3": "VLC 3",
|
"VLC_3": "VLC 3",
|
||||||
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
||||||
},
|
},
|
||||||
"show_custom_menu_links": "Показать ссылки кастомного меню",
|
"show_custom_menu_links": "Показать ссылки пользовательского меню",
|
||||||
"show_large_home_carousel": "Показывать большую карусель (beta)",
|
"show_large_home_carousel": "Показывать большую карусель (beta)",
|
||||||
"hide_libraries": "Скрыть библиотеки",
|
"hide_libraries": "Скрыть библиотеки",
|
||||||
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
"default_quality": "Качество по умолчанию",
|
"default_quality": "Качество по умолчанию",
|
||||||
"default_playback_speed": "Скорость воспроизведения по умолчанию",
|
"default_playback_speed": "Скорость воспроизведения по умолчанию",
|
||||||
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод",
|
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод",
|
||||||
"max_auto_play_episode_count": "Максимальное количество автовоспроизведения эпизодов",
|
"max_auto_play_episode_count": "Максимальное количество авто воспроизводимых эпизодов",
|
||||||
"disabled": "Отключено"
|
"disabled": "Отключено"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@@ -319,9 +319,9 @@
|
|||||||
"playback_description": "Настройте воспроизведение музыки.",
|
"playback_description": "Настройте воспроизведение музыки.",
|
||||||
"prefer_downloaded": "Предпочитать скачанные песни",
|
"prefer_downloaded": "Предпочитать скачанные песни",
|
||||||
"caching_title": "Кеширование",
|
"caching_title": "Кеширование",
|
||||||
"caching_description": "Автоматически предкешировать следующие треки для стабильного воспроизведения.",
|
"caching_description": "Автоматически кешировать следующие треки для стабильного воспроизведения.",
|
||||||
"lookahead_enabled": "Включить предкеширование",
|
"lookahead_enabled": "Включить предкеширование",
|
||||||
"lookahead_count": "Сколько предкешировать",
|
"lookahead_count": "Сколько треков предкешировать",
|
||||||
"max_cache_size": "Максимальное число предкешированных треков"
|
"max_cache_size": "Максимальное число предкешированных треков"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -329,8 +329,8 @@
|
|||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
||||||
"server_url": "URL сервера",
|
"server_url": "URL сервера",
|
||||||
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
|
"server_url_hint": "Пример: http(s)://your-host.url\n(добавьте порт если необходимо)",
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
"server_url_placeholder": "Seerr URL...",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
||||||
"login_button": "Войти",
|
"login_button": "Войти",
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Включить Marlin Search ",
|
"enable_marlin_search": "Включить Marlin Search",
|
||||||
"url": "URL-адрес",
|
"url": "URL-адрес",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
||||||
@@ -399,13 +399,13 @@
|
|||||||
"size_used": "{{used}} из {{total}} использовано",
|
"size_used": "{{used}} из {{total}} использовано",
|
||||||
"delete_all_downloaded_files": "Удалить все загруженные файлы",
|
"delete_all_downloaded_files": "Удалить все загруженные файлы",
|
||||||
"music_cache_title": "Кеш музыки",
|
"music_cache_title": "Кеш музыки",
|
||||||
"music_cache_description": "Автоматически прекешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
|
"music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
|
||||||
"enable_music_cache": "Кешировать музыку",
|
"enable_music_cache": "Кешировать музыку",
|
||||||
"clear_music_cache": "Очистить кеш музыки",
|
"clear_music_cache": "Очистить кеш музыки",
|
||||||
"music_cache_size": "{{size}} кешировано",
|
"music_cache_size": "Кешировано: {{size}}",
|
||||||
"music_cache_cleared": "Кеш музыки очищен",
|
"music_cache_cleared": "Кеш музыки очищен",
|
||||||
"delete_all_downloaded_songs": "Удалить все скачанные песни",
|
"delete_all_downloaded_songs": "Удалить все скачанные песни",
|
||||||
"downloaded_songs_size": "{{size}} скачано",
|
"downloaded_songs_size": "Скачано: {{size}}",
|
||||||
"downloaded_songs_deleted": "Скачанные песни удалены"
|
"downloaded_songs_deleted": "Скачанные песни удалены"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"logs_title": "Логи",
|
"logs_title": "Логи",
|
||||||
"export_logs": "Экспорт журналов",
|
"export_logs": "Сохранить логи",
|
||||||
"click_for_more_info": "Нажмите для получения дополнительной информации",
|
"click_for_more_info": "Нажмите для получения дополнительной информации",
|
||||||
"level": "Уровень",
|
"level": "Уровень",
|
||||||
"no_logs_available": "Логи не доступны",
|
"no_logs_available": "Логи не доступны",
|
||||||
@@ -453,7 +453,7 @@
|
|||||||
"no_active_downloads": "Нет активных загрузок",
|
"no_active_downloads": "Нет активных загрузок",
|
||||||
"active_downloads": "Активные",
|
"active_downloads": "Активные",
|
||||||
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
||||||
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
|
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки контента. Пожалуйста, удалите весь скачанный контент и попробуйте заново.",
|
||||||
"back": "Назад",
|
"back": "Назад",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"something_went_wrong": "Что-то пошло не так",
|
"something_went_wrong": "Что-то пошло не так",
|
||||||
@@ -465,29 +465,29 @@
|
|||||||
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
||||||
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
||||||
"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_deleted": "Загруженный контент удалён",
|
||||||
"download_cancelled": "Загрузка отменена",
|
"download_cancelled": "Загрузка отменена",
|
||||||
"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_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
||||||
"download_completed_for_item": "{{item}} успешно загружен",
|
"download_completed_for_item": "{{item}} успешно загружен",
|
||||||
"download_started_for_item": "Загрузка началась для {{item}}",
|
"download_started_for_item": "Загрузка {{item}} началась",
|
||||||
"failed_to_start_download": "Не удалось начать загрузку",
|
"failed_to_start_download": "Не удалось начать загрузку",
|
||||||
"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": "Все файлы, папки, и задачи были успешно удалены",
|
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
|
||||||
"failed_to_clean_cache_directory": "Не удалось очистить директорию кэша",
|
"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": "В загрузки",
|
"go_to_downloads": "В загрузки",
|
||||||
"file_deleted": "{{item}} удалён"
|
"file_deleted": "Удалено: {{item}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Поиск...",
|
"search": "Поиск...",
|
||||||
"x_items": "{{count}} элементов",
|
"x_items": "Элементов: {{count}}",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
"discover": "Найти новое",
|
"discover": "Найти новое",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
@@ -529,14 +529,14 @@
|
|||||||
"request_series": "Запросить сериалы",
|
"request_series": "Запросить сериалы",
|
||||||
"recently_added": "Недавно добавлено",
|
"recently_added": "Недавно добавлено",
|
||||||
"recent_requests": "Недавно запрошено",
|
"recent_requests": "Недавно запрошено",
|
||||||
"plex_watchlist": "Список просмотра с Plex",
|
"plex_watchlist": "Список просмотра Plex",
|
||||||
"trending": "В тренде",
|
"trending": "В тренде",
|
||||||
"popular_movies": "Популярные фильмы",
|
"popular_movies": "Популярные фильмы",
|
||||||
"movie_genres": "Популярные жанры",
|
"movie_genres": "Популярные жанры",
|
||||||
"upcoming_movies": "Предстоящие фильмы",
|
"upcoming_movies": "Предстоящие фильмы",
|
||||||
"studios": "Студии",
|
"studios": "Студии",
|
||||||
"popular_tv": "Популярные сериалы",
|
"popular_tv": "Популярные сериалы",
|
||||||
"tv_genres": "жанры сериалов",
|
"tv_genres": "Жанры сериалов",
|
||||||
"upcoming_tv": "Предстоящие сериалы",
|
"upcoming_tv": "Предстоящие сериалы",
|
||||||
"networks": "Сети",
|
"networks": "Сети",
|
||||||
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
||||||
@@ -556,7 +556,7 @@
|
|||||||
"movies": "Фильмы",
|
"movies": "Фильмы",
|
||||||
"series": "Сериалы",
|
"series": "Сериалы",
|
||||||
"boxsets": "Коллекции",
|
"boxsets": "Коллекции",
|
||||||
"items": "элементы"
|
"items": "Элементы"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"display": "Отображать",
|
"display": "Отображать",
|
||||||
@@ -565,7 +565,7 @@
|
|||||||
"image_style": "Стиль изображения",
|
"image_style": "Стиль изображения",
|
||||||
"poster": "Постер",
|
"poster": "Постер",
|
||||||
"cover": "Обложка",
|
"cover": "Обложка",
|
||||||
"show_titles": "Показывать загаловки",
|
"show_titles": "Показывать заголовки",
|
||||||
"show_stats": "Показывать статистику"
|
"show_stats": "Показывать статистику"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
"sort_by": "Сортировка",
|
"sort_by": "Сортировка",
|
||||||
"filter_by": "Фильтр",
|
"filter_by": "Фильтр",
|
||||||
"sort_order": "Порядок",
|
"sort_order": "Порядок",
|
||||||
"tags": "Тэги"
|
"tags": "Теги"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Нет",
|
"downloaded_file_no": "Нет",
|
||||||
"downloaded_file_cancel": "Отмена"
|
"downloaded_file_cancel": "Отмена"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Далее",
|
"next_up": "Далее",
|
||||||
"no_items_to_display": "Нет элементов для отображения",
|
"no_items_to_display": "Нет элементов для отображения",
|
||||||
@@ -624,7 +630,7 @@
|
|||||||
"no_similar_items_found": "Похожие элементы не найдены",
|
"no_similar_items_found": "Похожие элементы не найдены",
|
||||||
"video": "Видео",
|
"video": "Видео",
|
||||||
"more_details": "Больше деталей",
|
"more_details": "Больше деталей",
|
||||||
"media_options": "Media Options",
|
"media_options": "Опции медиа",
|
||||||
"quality": "Качество",
|
"quality": "Качество",
|
||||||
"audio": "Звук",
|
"audio": "Звук",
|
||||||
"subtitles": "Субтитры",
|
"subtitles": "Субтитры",
|
||||||
@@ -685,12 +691,12 @@
|
|||||||
"currently_streaming_on": "Сейчас доступно на",
|
"currently_streaming_on": "Сейчас доступно на",
|
||||||
"advanced": "Продвинутое",
|
"advanced": "Продвинутое",
|
||||||
"request_as": "Запросить как",
|
"request_as": "Запросить как",
|
||||||
"tags": "Тэги",
|
"tags": "Теги",
|
||||||
"quality_profile": "Профиль качества",
|
"quality_profile": "Профиль качества",
|
||||||
"root_folder": "Корневая папка",
|
"root_folder": "Корневая папка",
|
||||||
"season_all": "Сезон (все)",
|
"season_all": "Сезон (все)",
|
||||||
"season_number": "Сезон {{season_number}}",
|
"season_number": "Сезон {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} серий",
|
"number_episodes": "Серий: {{episode_number}}",
|
||||||
"born": "Рожден",
|
"born": "Рожден",
|
||||||
"appearances": "Появления",
|
"appearances": "Появления",
|
||||||
"approve": "Одобрить",
|
"approve": "Одобрить",
|
||||||
@@ -700,11 +706,11 @@
|
|||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
||||||
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
||||||
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
|
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера Seerr",
|
||||||
"issue_submitted": "Проблема отправлена!",
|
"issue_submitted": "Проблема отправлена!",
|
||||||
"requested_item": "Запрошено {{item}}!",
|
"requested_item": "Запрошено {{item}}!",
|
||||||
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
||||||
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!",
|
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиа!",
|
||||||
"request_approved": "Запрос одобрен!",
|
"request_approved": "Запрос одобрен!",
|
||||||
"request_declined": "Запрос отклонён!",
|
"request_declined": "Запрос отклонён!",
|
||||||
"failed_to_approve_request": "Не удалось одобрить запрос",
|
"failed_to_approve_request": "Не удалось одобрить запрос",
|
||||||
@@ -801,7 +807,7 @@
|
|||||||
"name_label": "Название",
|
"name_label": "Название",
|
||||||
"name_placeholder": "Введите название списка",
|
"name_placeholder": "Введите название списка",
|
||||||
"description_label": "Описание",
|
"description_label": "Описание",
|
||||||
"description_placeholder": "Введите описание (не обязательно)",
|
"description_placeholder": "Введите описание (необязательно)",
|
||||||
"is_public_label": "Публичный",
|
"is_public_label": "Публичный",
|
||||||
"is_public_description": "Разрешить остальным пользователям видеть этот список",
|
"is_public_description": "Разрешить остальным пользователям видеть этот список",
|
||||||
"allowed_type_label": "Тип контента",
|
"allowed_type_label": "Тип контента",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Nej",
|
"downloaded_file_no": "Nej",
|
||||||
"downloaded_file_cancel": "Avbryt"
|
"downloaded_file_cancel": "Avbryt"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Näst på tur",
|
"next_up": "Näst på tur",
|
||||||
"no_items_to_display": "Inga Artiklar Att Visa",
|
"no_items_to_display": "Inga Artiklar Att Visa",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"no_items_to_display": "No Items to Display",
|
"no_items_to_display": "No Items to Display",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "wej",
|
"next_up": "wej",
|
||||||
"no_items_to_display": "Doch pagh HochlaH",
|
"no_items_to_display": "Doch pagh HochlaH",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "Hayır",
|
"downloaded_file_no": "Hayır",
|
||||||
"downloaded_file_cancel": "Vazgeç"
|
"downloaded_file_cancel": "Vazgeç"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Sıradaki",
|
"next_up": "Sıradaki",
|
||||||
"no_items_to_display": "Görüntülenecek öğe yok",
|
"no_items_to_display": "Görüntülenecek öğe yok",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Далі",
|
"next_up": "Далі",
|
||||||
"no_items_to_display": "Немає елементів для відображення",
|
"no_items_to_display": "Немає елементів для відображення",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Tiếp theo",
|
"next_up": "Tiếp theo",
|
||||||
"no_items_to_display": "Không có nội dung để hiển thị",
|
"no_items_to_display": "Không có nội dung để hiển thị",
|
||||||
|
|||||||
@@ -610,6 +610,12 @@
|
|||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"no_items_to_display": "No Items to Display",
|
"no_items_to_display": "No Items to Display",
|
||||||
|
|||||||
138
utils/chapters.test.ts
Normal file
138
utils/chapters.test.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
chapterMarkers,
|
||||||
|
chapterNameAt,
|
||||||
|
chapterStartsMs,
|
||||||
|
currentChapterIndex,
|
||||||
|
formatChapterTime,
|
||||||
|
sortedChapters,
|
||||||
|
} from "./chapters";
|
||||||
|
|
||||||
|
// Helper: a ChapterInfo with a start in milliseconds.
|
||||||
|
const ch = (ms: number, name?: string) => ({
|
||||||
|
StartPositionTicks: ms * 10000,
|
||||||
|
Name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chapterMarkers", () => {
|
||||||
|
test("maps chapters to position + percent", () => {
|
||||||
|
expect(chapterMarkers([ch(0), ch(30_000), ch(60_000)], 120_000)).toEqual([
|
||||||
|
{ positionMs: 0, percent: 0 },
|
||||||
|
{ positionMs: 30_000, percent: 25 },
|
||||||
|
{ positionMs: 60_000, percent: 50 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("drops chapters past the duration", () => {
|
||||||
|
expect(chapterMarkers([ch(0), ch(200_000)], 120_000)).toEqual([
|
||||||
|
{ positionMs: 0, percent: 0 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns [] when duration is 0 or chapters missing", () => {
|
||||||
|
expect(chapterMarkers([ch(0)], 0)).toEqual([]);
|
||||||
|
expect(chapterMarkers(null, 120_000)).toEqual([]);
|
||||||
|
expect(chapterMarkers(undefined, 120_000)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("excludes a chapter exactly at the duration", () => {
|
||||||
|
expect(chapterMarkers([ch(0), ch(120_000)], 120_000)).toEqual([
|
||||||
|
{ positionMs: 0, percent: 0 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips chapters with no StartPositionTicks", () => {
|
||||||
|
expect(
|
||||||
|
chapterMarkers([{ StartPositionTicks: undefined }, ch(30_000)], 120_000),
|
||||||
|
).toEqual([{ positionMs: 30_000, percent: 25 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("currentChapterIndex", () => {
|
||||||
|
const chapters = [ch(0), ch(30_000), ch(60_000)];
|
||||||
|
test("returns the chapter containing the position", () => {
|
||||||
|
expect(currentChapterIndex(0, chapters)).toBe(0);
|
||||||
|
expect(currentChapterIndex(15_000, chapters)).toBe(0);
|
||||||
|
expect(currentChapterIndex(30_000, chapters)).toBe(1);
|
||||||
|
expect(currentChapterIndex(90_000, chapters)).toBe(2);
|
||||||
|
});
|
||||||
|
test("returns -1 before the first chapter and for no chapters", () => {
|
||||||
|
expect(currentChapterIndex(-5, chapters)).toBe(-1);
|
||||||
|
expect(currentChapterIndex(10_000, [])).toBe(-1);
|
||||||
|
expect(currentChapterIndex(10_000, null)).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sortedChapters", () => {
|
||||||
|
test("pairs each chapter with its ms start, sorted ascending", () => {
|
||||||
|
const a = ch(60_000, "C");
|
||||||
|
const b = ch(0, "A");
|
||||||
|
const c = ch(30_000, "B");
|
||||||
|
expect(sortedChapters([a, b, c])).toEqual([
|
||||||
|
{ chapter: b, positionMs: 0 },
|
||||||
|
{ chapter: c, positionMs: 30_000 },
|
||||||
|
{ chapter: a, positionMs: 60_000 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test("returns [] for null/undefined", () => {
|
||||||
|
expect(sortedChapters(null)).toEqual([]);
|
||||||
|
expect(sortedChapters(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chapterStartsMs", () => {
|
||||||
|
test("returns sorted ms positions", () => {
|
||||||
|
expect(chapterStartsMs([ch(60_000), ch(0), ch(30_000)])).toEqual([
|
||||||
|
0, 30_000, 60_000,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips entries without StartPositionTicks", () => {
|
||||||
|
expect(
|
||||||
|
chapterStartsMs([ch(30_000), { StartPositionTicks: undefined }, ch(0)]),
|
||||||
|
).toEqual([0, 30_000]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns [] for null/undefined/empty", () => {
|
||||||
|
expect(chapterStartsMs(null)).toEqual([]);
|
||||||
|
expect(chapterStartsMs(undefined)).toEqual([]);
|
||||||
|
expect(chapterStartsMs([])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chapterNameAt", () => {
|
||||||
|
const named = [
|
||||||
|
{ StartPositionTicks: 0, Name: "Intro" },
|
||||||
|
{ StartPositionTicks: 30_000 * 10000, Name: "Action" },
|
||||||
|
{ StartPositionTicks: 60_000 * 10000, Name: "Outro" },
|
||||||
|
];
|
||||||
|
|
||||||
|
test("returns the chapter name for the active position", () => {
|
||||||
|
expect(chapterNameAt(0, named)).toBe("Intro");
|
||||||
|
expect(chapterNameAt(15_000, named)).toBe("Intro");
|
||||||
|
expect(chapterNameAt(45_000, named)).toBe("Action");
|
||||||
|
expect(chapterNameAt(90_000, named)).toBe("Outro");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null before the first chapter", () => {
|
||||||
|
expect(chapterNameAt(-1, named)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for null/undefined/empty chapters", () => {
|
||||||
|
expect(chapterNameAt(10_000, null)).toBeNull();
|
||||||
|
expect(chapterNameAt(10_000, undefined)).toBeNull();
|
||||||
|
expect(chapterNameAt(10_000, [])).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when the active chapter has no Name", () => {
|
||||||
|
expect(chapterNameAt(15_000, [ch(0), ch(30_000)])).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatChapterTime", () => {
|
||||||
|
test("formats m:ss and h:mm:ss", () => {
|
||||||
|
expect(formatChapterTime(65_000)).toBe("1:05");
|
||||||
|
expect(formatChapterTime(3_725_000)).toBe("1:02:05");
|
||||||
|
expect(formatChapterTime(-100)).toBe("0:00");
|
||||||
|
});
|
||||||
|
});
|
||||||
97
utils/chapters.ts
Normal file
97
utils/chapters.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Pure helpers for Jellyfin chapter markers. Dependency-free so they are
|
||||||
|
* unit-testable under `bun test`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { ticksToMs } from "@/utils/time";
|
||||||
|
|
||||||
|
export interface ChapterMarker {
|
||||||
|
/** Chapter start, in milliseconds. */
|
||||||
|
positionMs: number;
|
||||||
|
/** Chapter start as a percentage (0-100) of the media duration. */
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChapterEntry {
|
||||||
|
chapter: ChapterInfo;
|
||||||
|
/** Chapter start, in milliseconds. */
|
||||||
|
positionMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Chapters paired with their millisecond start, sorted ascending by start. */
|
||||||
|
export const sortedChapters = (
|
||||||
|
chapters: ChapterInfo[] | null | undefined,
|
||||||
|
): ChapterEntry[] =>
|
||||||
|
(chapters ?? [])
|
||||||
|
.filter((c) => c.StartPositionTicks != null)
|
||||||
|
.map((chapter) => ({
|
||||||
|
chapter,
|
||||||
|
positionMs: ticksToMs(chapter.StartPositionTicks),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.positionMs - b.positionMs);
|
||||||
|
|
||||||
|
/** Chapter start positions in milliseconds, ascending. */
|
||||||
|
export const chapterStartsMs = (
|
||||||
|
chapters: ChapterInfo[] | null | undefined,
|
||||||
|
): number[] =>
|
||||||
|
(chapters ?? [])
|
||||||
|
.filter((c) => c.StartPositionTicks != null)
|
||||||
|
.map((c) => ticksToMs(c.StartPositionTicks))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
/** Chapter markers within [0, durationMs]; empty when duration is unknown. */
|
||||||
|
export const chapterMarkers = (
|
||||||
|
chapters: ChapterInfo[] | null | undefined,
|
||||||
|
durationMs: number,
|
||||||
|
): ChapterMarker[] => {
|
||||||
|
if (durationMs <= 0) return [];
|
||||||
|
return chapterStartsMs(chapters)
|
||||||
|
.filter((ms) => ms >= 0 && ms < durationMs)
|
||||||
|
.map((ms) => ({ positionMs: ms, percent: (ms / durationMs) * 100 }));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Index of the chapter containing `positionMs`, or -1 if before the first. */
|
||||||
|
export const currentChapterIndex = (
|
||||||
|
positionMs: number,
|
||||||
|
chapters: ChapterInfo[] | null | undefined,
|
||||||
|
): number => {
|
||||||
|
const starts = chapterStartsMs(chapters);
|
||||||
|
let index = -1;
|
||||||
|
for (let i = 0; i < starts.length; i++) {
|
||||||
|
if (positionMs >= starts[i]) index = i;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Name of the chapter containing `positionMs`, or null if none / unnamed. */
|
||||||
|
export const chapterNameAt = (
|
||||||
|
positionMs: number,
|
||||||
|
chapters: ChapterInfo[] | null | undefined,
|
||||||
|
): string | null => {
|
||||||
|
// Sort once, derive both the active index and the entry from the same array
|
||||||
|
// — `chapterNameAt` runs on every playback tick, so paying for one `sort()`
|
||||||
|
// instead of two is worth the duplication of the index loop here.
|
||||||
|
const sorted = sortedChapters(chapters);
|
||||||
|
let idx = -1;
|
||||||
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
|
if (positionMs >= sorted[i].positionMs) idx = i;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (idx < 0) return null;
|
||||||
|
const name = sorted[idx]?.chapter.Name;
|
||||||
|
return name && name.length > 0 ? name : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** `m:ss` (or `h:mm:ss` past an hour) label for a millisecond position. */
|
||||||
|
export const formatChapterTime = (positionMs: number): string => {
|
||||||
|
const total = Math.max(0, Math.floor(positionMs / 1000));
|
||||||
|
const hours = Math.floor(total / 3600);
|
||||||
|
const minutes = Math.floor((total % 3600) / 60);
|
||||||
|
const seconds = total % 60;
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return hours > 0
|
||||||
|
? `${hours}:${pad(minutes)}:${pad(seconds)}`
|
||||||
|
: `${minutes}:${pad(seconds)}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user