fix(ui): harden settings focus and semantics
This commit is contained in:
+519
-11
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
// Generated from USB HID Usage Tables — do not edit by hand.
|
||||
// Generated by dart run scripts/generate_hid_key_labels.dart from scripts/data/hid_key_labels.json; do not edit by hand.
|
||||
|
||||
/// Human-readable labels for physical keyboard keys, keyed by USB HID usage code.
|
||||
const hidKeyLabels = <int, String>{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated from iso_639_codes.json — do not edit by hand.
|
||||
// Generated by dart run scripts/generate_iso_639_data.dart from scripts/data/iso_639_codes.json; do not edit by hand.
|
||||
|
||||
class LanguageEntry {
|
||||
final String code1;
|
||||
|
||||
@@ -106,7 +106,22 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
FocusNode? getFocusNode(int index) => index >= 0 && index < _focusNodes.length ? _focusNodes[index] : null;
|
||||
|
||||
void requestFocusOnFirst() {
|
||||
if (_focusNodes.isNotEmpty) _focusNodes.first.requestFocus();
|
||||
final index = _nextEnabledIndex(-1);
|
||||
if (index != null) _focusNodes[index].requestFocus();
|
||||
}
|
||||
|
||||
int? _previousEnabledIndex(int index) {
|
||||
for (var candidate = index - 1; candidate >= 0; candidate--) {
|
||||
if (widget.actions[candidate].onPressed != null) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _nextEnabledIndex(int index) {
|
||||
for (var candidate = index + 1; candidate < widget.actions.length; candidate++) {
|
||||
if (widget.actions[candidate].onPressed != null) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -202,10 +217,10 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
|
||||
Widget _buildButton(int index, bool isKeyboard, Duration duration) {
|
||||
final action = widget.actions[index];
|
||||
final enabled = action.onPressed != null;
|
||||
final isFocused = _focusStates[index];
|
||||
final showFocus = isFocused && isKeyboard;
|
||||
final opacity = isKeyboard && _hasAnyFocus && !isFocused ? 0.6 : 1.0;
|
||||
|
||||
final buildState = FocusableActionBuildState(
|
||||
focusNode: _focusNodes[index],
|
||||
isFocused: isFocused,
|
||||
@@ -217,28 +232,29 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
|
||||
return Focus(
|
||||
focusNode: _focusNodes[index],
|
||||
autofocus: action.autofocus,
|
||||
canRequestFocus: enabled,
|
||||
autofocus: action.autofocus && enabled,
|
||||
descendantsAreFocusable: false,
|
||||
onKeyEvent: (node, event) {
|
||||
if (widget.onBack != null) {
|
||||
final backResult = handleBackKeyAction(event, widget.onBack!);
|
||||
if (backResult != KeyEventResult.ignored) return backResult;
|
||||
}
|
||||
final previousIndex = _previousEnabledIndex(index);
|
||||
final nextIndex = _nextEnabledIndex(index);
|
||||
return dpadKeyHandler(
|
||||
onSelect: action.onPressed,
|
||||
onLeft: index > 0 ? () => _focusNodes[index - 1].requestFocus() : widget.onNavigateLeft,
|
||||
onRight: index < _focusNodes.length - 1
|
||||
? () => _focusNodes[index + 1].requestFocus()
|
||||
: widget.onNavigateRight,
|
||||
onLeft: previousIndex != null ? () => _focusNodes[previousIndex].requestFocus() : widget.onNavigateLeft,
|
||||
onRight: nextIndex != null ? () => _focusNodes[nextIndex].requestFocus() : widget.onNavigateRight,
|
||||
onDown: widget.onNavigateDown,
|
||||
onUp: widget.onNavigateUp,
|
||||
// Consume LEFT/RIGHT at the row's first/last button when no edge
|
||||
// callback is wired, so focus can't fall off the row (#1181).
|
||||
// Consume LEFT/RIGHT at the row's first/last enabled button when no
|
||||
// edge callback is wired, so focus can't fall off the row (#1181).
|
||||
trapHorizontalEdges: true,
|
||||
)(node, event);
|
||||
},
|
||||
child: ClickableCursor(
|
||||
enabled: action.onPressed != null || action.child != null || customChild != null,
|
||||
enabled: enabled,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showFocus ? 1.0 : opacity,
|
||||
duration: duration,
|
||||
|
||||
@@ -50,12 +50,14 @@ class _FocusableButtonState extends State<FocusableButton> {
|
||||
final isKeyboard = InputModeTracker.isKeyboardMode(context);
|
||||
final showFocus = _isFocused && isKeyboard;
|
||||
final duration = FocusTheme.getAnimationDuration(context);
|
||||
final enabled = widget.onPressed != null;
|
||||
// In dpad mode: focused = full opacity, unfocused = dimmed
|
||||
final opacity = isKeyboard && !_isFocused ? 0.6 : 1.0;
|
||||
|
||||
return FocusableWrapper(
|
||||
autofocus: widget.autofocus,
|
||||
autofocus: widget.autofocus && enabled,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: enabled,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
useBackgroundFocus: widget.useBackgroundFocus,
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../utils/platform_detector.dart';
|
||||
import '../utils/text_input_diagnostics.dart';
|
||||
import '../widgets/tv_virtual_keyboard.dart';
|
||||
import 'dpad_navigator.dart';
|
||||
import 'key_event_utils.dart';
|
||||
import 'owned_focus_node_binding.dart';
|
||||
|
||||
bool _usesTvKeyboard(bool enableTvKeyboard) => enableTvKeyboard && PlatformDetector.isTV();
|
||||
@@ -360,16 +361,20 @@ KeyEventResult _moveCaretHorizontally(TextEditingController controller, int delt
|
||||
}
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
final offset = delta < 0
|
||||
? (selection.start < selection.end ? selection.start : selection.end)
|
||||
: (selection.start > selection.end ? selection.start : selection.end);
|
||||
controller.selection = TextSelection.collapsed(offset: offset);
|
||||
final range = expandToGraphemeRange(value.text, selection);
|
||||
controller.selection = TextSelection.collapsed(offset: delta < 0 ? range.start : range.end);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
final nextOffset = selection.extentOffset + delta;
|
||||
if (nextOffset < 0 || nextOffset > value.text.length) return KeyEventResult.ignored;
|
||||
controller.selection = TextSelection.collapsed(offset: nextOffset);
|
||||
final offset = selection.extentOffset.clamp(0, value.text.length);
|
||||
if ((delta < 0 && offset == 0) || (delta > 0 && offset == value.text.length)) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final codeUnitRange = delta < 0
|
||||
? TextRange(start: offset - 1, end: offset)
|
||||
: TextRange(start: offset, end: offset + 1);
|
||||
final range = expandToGraphemeRange(value.text, codeUnitRange);
|
||||
controller.selection = TextSelection.collapsed(offset: delta < 0 ? range.start : range.end);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@@ -439,25 +444,18 @@ void _backspace({
|
||||
ValueChanged<String>? onChanged,
|
||||
}) {
|
||||
final value = controller.value;
|
||||
final range = _normalizedSelectionRange(value);
|
||||
final start = range.start;
|
||||
final end = range.end;
|
||||
if (start != end) {
|
||||
final selectionRange = _normalizedSelectionRange(value);
|
||||
final start = selectionRange.start;
|
||||
final end = selectionRange.end;
|
||||
if (start == end && start == 0) return;
|
||||
|
||||
final codeUnitRange = start == end ? TextRange(start: start - 1, end: start) : TextRange(start: start, end: end);
|
||||
final range = expandToGraphemeRange(value.text, codeUnitRange);
|
||||
if (range.isCollapsed) return;
|
||||
_replaceTextRange(
|
||||
controller,
|
||||
start,
|
||||
end,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (start == 0) return;
|
||||
_replaceTextRange(
|
||||
controller,
|
||||
start - 1,
|
||||
start,
|
||||
range.start,
|
||||
range.end,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChanged,
|
||||
@@ -471,25 +469,18 @@ void _deleteForward({
|
||||
ValueChanged<String>? onChanged,
|
||||
}) {
|
||||
final value = controller.value;
|
||||
final range = _normalizedSelectionRange(value);
|
||||
final start = range.start;
|
||||
final end = range.end;
|
||||
if (start != end) {
|
||||
final selectionRange = _normalizedSelectionRange(value);
|
||||
final start = selectionRange.start;
|
||||
final end = selectionRange.end;
|
||||
if (start == end && start >= value.text.length) return;
|
||||
|
||||
final codeUnitRange = start == end ? TextRange(start: start, end: start + 1) : TextRange(start: start, end: end);
|
||||
final range = expandToGraphemeRange(value.text, codeUnitRange);
|
||||
if (range.isCollapsed) return;
|
||||
_replaceTextRange(
|
||||
controller,
|
||||
start,
|
||||
end,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (start >= value.text.length) return;
|
||||
_replaceTextRange(
|
||||
controller,
|
||||
start,
|
||||
start + 1,
|
||||
range.start,
|
||||
range.end,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChanged,
|
||||
|
||||
@@ -85,6 +85,12 @@ class FocusableWrapper extends StatefulWidget {
|
||||
/// Optional semantic label for accessibility.
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Optional current value announced after [semanticLabel].
|
||||
final String? semanticValue;
|
||||
|
||||
/// Optional checked state for toggle-style controls.
|
||||
final bool? checked;
|
||||
|
||||
/// Whether the wrapper can receive focus.
|
||||
final bool canRequestFocus;
|
||||
|
||||
@@ -147,6 +153,8 @@ class FocusableWrapper extends StatefulWidget {
|
||||
this.scrollAlignment = 0.5,
|
||||
this.useComfortableZone = false,
|
||||
this.semanticLabel,
|
||||
this.semanticValue,
|
||||
this.checked,
|
||||
this.canRequestFocus = true,
|
||||
this.onKeyEvent,
|
||||
this.enableLongPress = false,
|
||||
@@ -521,7 +529,17 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
|
||||
// Add semantics if label provided
|
||||
if (widget.semanticLabel != null) {
|
||||
result = Semantics(label: widget.semanticLabel, button: widget.onSelect != null, child: result);
|
||||
result = Semantics(
|
||||
label: widget.semanticLabel,
|
||||
value: widget.semanticValue,
|
||||
button: true,
|
||||
enabled: widget.onSelect != null,
|
||||
checked: widget.checked,
|
||||
onTap: widget.onSelect,
|
||||
onLongPress: widget.onLongPress,
|
||||
excludeSemantics: true,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.onSelect != null || widget.onLongPress != null) {
|
||||
|
||||
@@ -121,27 +121,33 @@ class _InputModeTrackerState extends State<InputModeTracker> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// On Android TV, don't switch to pointer mode from pointer events
|
||||
// as D-pad can generate synthetic pointer events that would incorrectly
|
||||
// trigger pointer mode and show a cursor instead of D-pad focus navigation.
|
||||
// Desktop is exempt even in force-TV mode: its pointer events come from a
|
||||
// real mouse, which should keep flipping modes (and the cursor) as usual.
|
||||
// Non-desktop TVs keep keyboard mode across synthetic pointer events, but
|
||||
// their controls remain pointer-reachable for engine-generated taps.
|
||||
if (TvDetectionService.isTVSync() && !PlatformDetector.isDesktopOS()) {
|
||||
return _InputModeProvider(mode: _mode, child: widget.child);
|
||||
}
|
||||
|
||||
return Listener(
|
||||
// Switch to pointer mode on mouse activity
|
||||
onPointerDown: (_) => _setMode(InputMode.pointer),
|
||||
onPointerHover: (_) => _setMode(InputMode.pointer),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Stack(
|
||||
alignment: Alignment.topLeft,
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
_InputModeProvider(mode: _mode, child: widget.child),
|
||||
// Hide the desktop cursor in keyboard mode without excluding the
|
||||
// application subtree from the current pointer hit test.
|
||||
if (_mode == InputMode.keyboard)
|
||||
const Positioned.fill(
|
||||
child: MouseRegion(
|
||||
cursor: _mode == InputMode.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||
child: IgnorePointer(
|
||||
ignoring: _mode == InputMode.keyboard,
|
||||
child: _InputModeProvider(mode: _mode, child: widget.child),
|
||||
cursor: SystemMouseCursors.none,
|
||||
opaque: false,
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,28 @@ KeyEventResult handleOneShotSelect(KeyEvent event, VoidCallback onActivate) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
/// Expands a UTF-16 [range] to whole extended grapheme clusters in [text].
|
||||
///
|
||||
/// Flutter selections use UTF-16 code-unit offsets. Custom editors must pass a
|
||||
/// non-empty range containing the code units they intend to replace; the
|
||||
/// returned range is normalized, clamped, and safe to use with
|
||||
/// [String.replaceRange] without splitting a user-perceived character.
|
||||
TextRange expandToGraphemeRange(String text, TextRange range) {
|
||||
if (text.isEmpty) return TextRange.empty;
|
||||
|
||||
final first = range.start.clamp(0, text.length);
|
||||
final second = range.end.clamp(0, text.length);
|
||||
final start = first <= second ? first : second;
|
||||
final end = first <= second ? second : first;
|
||||
if (start == end) return TextRange.collapsed(start);
|
||||
|
||||
final boundary = CharacterBoundary(text);
|
||||
return TextRange(
|
||||
start: boundary.getLeadingTextBoundaryAt(start) ?? 0,
|
||||
end: boundary.getTrailingTextBoundaryAt(end - 1) ?? text.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [FocusOnKeyEventCallback] that dispatches d-pad / arrow keys to
|
||||
/// the provided directional callbacks.
|
||||
///
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/// Manages focus memory for hub navigation.
|
||||
/// Manages focus memory for one browse surface.
|
||||
///
|
||||
/// Tracks two things:
|
||||
/// 1. Per-hub memory: Each hub remembers which item was last focused
|
||||
/// 2. Global column hint: When entering a hub that hasn't been visited,
|
||||
/// 2. Last column hint: When entering a hub that hasn't been visited,
|
||||
/// we use the column position from the last focused hub as a hint
|
||||
class HubFocusMemory {
|
||||
static final Map<String, int> _perHubMemory = {};
|
||||
static int _lastColumnHint = 0;
|
||||
final Map<String, int> _perHubMemory = {};
|
||||
int _lastColumnHint = 0;
|
||||
|
||||
static void setForHub(String hubKey, int index) {
|
||||
void setForHub(String hubKey, int index) {
|
||||
_perHubMemory[hubKey] = index;
|
||||
_lastColumnHint = index;
|
||||
}
|
||||
|
||||
/// Get the remembered index for a hub, or fall back to column hint
|
||||
static int getForHub(String hubKey, int itemCount) {
|
||||
int getForHub(String hubKey, int itemCount) {
|
||||
if (itemCount <= 0) return 0;
|
||||
|
||||
// If this hub has memory, use it
|
||||
@@ -26,16 +26,10 @@ class HubFocusMemory {
|
||||
return _lastColumnHint.clamp(0, itemCount - 1);
|
||||
}
|
||||
|
||||
/// Get only this hub's remembered index, without falling back to the global column hint.
|
||||
static int getForHubOnly(String hubKey, int itemCount, {int fallback = 0}) {
|
||||
/// Get only this hub's remembered index, without falling back to the last column hint.
|
||||
int getForHubOnly(String hubKey, int itemCount, {int fallback = 0}) {
|
||||
if (itemCount <= 0) return 0;
|
||||
final remembered = _perHubMemory[hubKey];
|
||||
return (remembered ?? fallback).clamp(0, itemCount - 1);
|
||||
}
|
||||
|
||||
/// Clear all memory (e.g., when leaving a screen)
|
||||
static void clear() {
|
||||
_perHubMemory.clear();
|
||||
_lastColumnHint = 0;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Изчакване на одобрение…",
|
||||
"quickConnectCancel": "Отказ",
|
||||
"quickConnectExpired": "Quick Connect изтече. Опитайте отново.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Отказ",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Релей сървър за гледане заедно",
|
||||
"watchTogetherRelayDescription": "Задай собствен релей сървър. Всички трябва да използват същия сървър.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Докладване на сривове",
|
||||
"crashReportingDescription": "Изпращай доклади за сривове, за да помогнеш за подобряване на приложението",
|
||||
"debugLogging": "Логове за отстраняване на грешки",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Експортирай настройките",
|
||||
"exportSettingsDescription": "Запази предпочитанията си във файл",
|
||||
"exportSettingsSuccess": "Настройките са експортирани",
|
||||
"exportSettingsFailed": "Настройките не можаха да бъдат експортирани",
|
||||
"importSettings": "Импортирай настройки",
|
||||
"importSettingsDescription": "Възстанови предпочитания от файл",
|
||||
"importSettingsConfirm": "Това ще замени текущите ви настройки. Продължавате ли?",
|
||||
"importSettingsSuccess": "Настройките са импортирани",
|
||||
"importSettingsFailed": "Настройките не можаха да бъдат импортирани",
|
||||
"importSettingsInvalidFile": "Този файл не е валиден експорт на настройки от Plezy",
|
||||
"importSettingsNoUser": "Влезте, преди да импортирате настройки",
|
||||
"shortcutsReset": "Клавишните комбинации са нулирани до подразбиране",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Продължителността трябва да е между ${min} и ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Клавишната комбинация вече е назначена за ${action}",
|
||||
"shortcutUpdated": "Клавишната комбинация е обновена за ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Автоматично прескачане",
|
||||
"autoSkipIntro": "Автоматично прескачане на интро",
|
||||
"autoSkipIntroDescription": "Автоматично прескачай интро маркери след няколко секунди",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Местоположението за изтегляния е променено",
|
||||
"downloadLocationReset": "Местоположението за изтегляния е върнато по подразбиране",
|
||||
"downloadLocationInvalid": "Избраната папка не е записваема",
|
||||
"downloadLocationSelectError": "Неуспешен избор на папка",
|
||||
"downloadLocationPickerUnavailable": "Изборът на папка не е наличен на това устройство",
|
||||
"downloadOnWifiOnly": "Изтегляне само през WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Предотвратявай изтегляния през мобилни данни",
|
||||
"autoRemoveWatchedDownloads": "Автоматично премахвай изгледаните изтегляния",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Яркост",
|
||||
"hexColor": "Шестнадесетичен цвят",
|
||||
"expandText": "Разгъни текста",
|
||||
"collapseText": "Свий текста"
|
||||
"collapseText": "Свий текста",
|
||||
"alphabetNavigation": "Навигация по азбуката",
|
||||
"alphabetScrollHint": "Плъзнете нагоре или надолу, за да преминете по буква",
|
||||
"rowColumnPosition": "Ред ${row} от ${rowCount}, колона ${column} от ${columnCount}",
|
||||
"rowPosition": "Ред ${row} от ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Разбъркано възпроизвеждане",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Маркирай като гледано",
|
||||
"markAsUnwatched": "Маркирай като негледано"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Аудио пътечка ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Аудио",
|
||||
"subtitlesLabel": "Субтитри",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Пусни следващото",
|
||||
"playButton": "Пусни",
|
||||
"pauseButton": "Пауза",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Превърти назад ${seconds} секунди",
|
||||
"seekForwardButton": "Превърти напред ${seconds} секунди",
|
||||
"previousButton": "Предишен епизод",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.",
|
||||
"liveStreamInterrupted": "Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.",
|
||||
"fileInfoNotAvailable": "Информацията за файла не е налична",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Грешка при зареждане на информация за файла: ${error}",
|
||||
"errorLoadingSeries": "Грешка при зареждане на сериала",
|
||||
"musicNotSupported": "Възпроизвеждането на музика все още не се поддържа",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Използвай връзка от друг профил. PIN-защитените профили изискват PIN.",
|
||||
"borrowEmpty": "Все още няма какво да се използва.",
|
||||
"borrowEmptySubtitle": "Първо свържете Plex или Jellyfin към друг профил.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "От ${displayName}",
|
||||
"borrowConnectionBorrowed": "Връзката е използвана.",
|
||||
"borrowFailed": "Неуспешно използване на връзка.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Любими",
|
||||
"reorderFavorites": "Пренареди любимите",
|
||||
"favoritesLoadFailed": "Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Присъедини се към текуща сесия",
|
||||
"watchFromStart": "Гледай от началото (преди ${minutes} мин)",
|
||||
"watchLive": "Гледай на живо",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Venter på godkendelse…",
|
||||
"quickConnectCancel": "Annullér",
|
||||
"quickConnectExpired": "Quick Connect er udløbet. Prøv igen.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuller",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Watch Together-relay",
|
||||
"watchTogetherRelayDescription": "Angiv en brugerdefineret relay. Alle skal bruge samme server.",
|
||||
"watchTogetherRelayHint": "https://min-relay.eksempel.dk",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Fejlrapportering",
|
||||
"crashReportingDescription": "Send fejlrapporter for at hjælpe med at forbedre appen",
|
||||
"debugLogging": "Fejlfindingslogning",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Eksportér indstillinger",
|
||||
"exportSettingsDescription": "Gem dine præferencer i en fil",
|
||||
"exportSettingsSuccess": "Indstillinger eksporteret",
|
||||
"exportSettingsFailed": "Kunne ikke eksportere indstillinger",
|
||||
"importSettings": "Importér indstillinger",
|
||||
"importSettingsDescription": "Gendan præferencer fra en fil",
|
||||
"importSettingsConfirm": "Dette vil erstatte dine nuværende indstillinger. Fortsæt?",
|
||||
"importSettingsSuccess": "Indstillinger importeret",
|
||||
"importSettingsFailed": "Kunne ikke importere indstillinger",
|
||||
"importSettingsInvalidFile": "Denne fil er ikke en gyldig Plezy-indstillingseksport",
|
||||
"importSettingsNoUser": "Log ind før import af indstillinger",
|
||||
"shortcutsReset": "Genveje nulstillet til standard",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Varighed skal være mellem ${min} og ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Genvej allerede tildelt til ${action}",
|
||||
"shortcutUpdated": "Genvej opdateret for ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Auto-spring",
|
||||
"autoSkipIntro": "Auto-spring intro",
|
||||
"autoSkipIntroDescription": "Spring automatisk intromarkører over efter få sekunder",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Downloadplacering ændret",
|
||||
"downloadLocationReset": "Downloadplacering nulstillet",
|
||||
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
|
||||
"downloadLocationSelectError": "Kunne ikke vælge mappe",
|
||||
"downloadLocationPickerUnavailable": "Mappevalg er ikke tilgængeligt på denne enhed",
|
||||
"downloadOnWifiOnly": "Download kun på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Forhindre downloads på mobildata",
|
||||
"autoRemoveWatchedDownloads": "Fjern sete downloads automatisk",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Lysstyrke",
|
||||
"hexColor": "Hexfarve",
|
||||
"expandText": "Udvid tekst",
|
||||
"collapseText": "Fold tekst sammen"
|
||||
"collapseText": "Fold tekst sammen",
|
||||
"alphabetNavigation": "Alfabetnavigation",
|
||||
"alphabetScrollHint": "Stryg op eller ned for at flytte ét bogstav",
|
||||
"rowColumnPosition": "Række ${row} af ${rowCount}, kolonne ${column} af ${columnCount}",
|
||||
"rowPosition": "Række ${row} af ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Afspil tilfældigt",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Markér som set",
|
||||
"markAsUnwatched": "Markér som uset"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Lydspor ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Lyd",
|
||||
"subtitlesLabel": "Undertekster",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Afspil næste",
|
||||
"playButton": "Afspil",
|
||||
"pauseButton": "Pause",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Spol ${seconds} sekunder tilbage",
|
||||
"seekForwardButton": "Spol ${seconds} sekunder frem",
|
||||
"previousButton": "Forrige episode",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.",
|
||||
"liveStreamInterrupted": "Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.",
|
||||
"fileInfoNotAvailable": "Filinfo ikke tilgængelig",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Fejl ved indlæsning af filinfo: ${error}",
|
||||
"errorLoadingSeries": "Fejl ved indlæsning af serie",
|
||||
"musicNotSupported": "Musikafspilning understøttes endnu ikke",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Lån en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.",
|
||||
"borrowEmpty": "Intet at låne endnu.",
|
||||
"borrowEmptySubtitle": "Forbind Plex eller Jellyfin til en anden profil først.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Fra ${displayName}",
|
||||
"borrowConnectionBorrowed": "Forbindelse lånt.",
|
||||
"borrowFailed": "Kunne ikke låne forbindelse.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Omarranger favoritter",
|
||||
"favoritesLoadFailed": "Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Deltag i igangværende session",
|
||||
"watchFromStart": "Se fra start (${minutes} min siden)",
|
||||
"watchLive": "Se live",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Warte auf Bestätigung…",
|
||||
"quickConnectCancel": "Abbrechen",
|
||||
"quickConnectExpired": "Quick Connect ist abgelaufen. Versuche es erneut.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Abbrechen",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Gemeinsam Schauen Relay",
|
||||
"watchTogetherRelayDescription": "Eigenes Relay festlegen. Alle müssen denselben Server verwenden.",
|
||||
"watchTogetherRelayHint": "https://mein-relay.beispiel.de",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Absturzberichte",
|
||||
"crashReportingDescription": "Absturzberichte senden, um die App zu verbessern",
|
||||
"debugLogging": "Debug-Protokollierung",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Einstellungen exportieren",
|
||||
"exportSettingsDescription": "Speichere deine Einstellungen in einer Datei",
|
||||
"exportSettingsSuccess": "Einstellungen exportiert",
|
||||
"exportSettingsFailed": "Einstellungen konnten nicht exportiert werden",
|
||||
"importSettings": "Einstellungen importieren",
|
||||
"importSettingsDescription": "Einstellungen aus einer Datei wiederherstellen",
|
||||
"importSettingsConfirm": "Dies ersetzt deine aktuellen Einstellungen. Fortfahren?",
|
||||
"importSettingsSuccess": "Einstellungen importiert",
|
||||
"importSettingsFailed": "Einstellungen konnten nicht importiert werden",
|
||||
"importSettingsInvalidFile": "Diese Datei ist kein gültiger Plezy-Einstellungsexport",
|
||||
"importSettingsNoUser": "Vor dem Import bitte anmelden",
|
||||
"shortcutsReset": "Tastenkürzel auf Standard zurückgesetzt",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Dauer muss zwischen ${min} und ${max} ${unit} liegen",
|
||||
"shortcutAlreadyAssigned": "Tastenkürzel bereits zugewiesen an ${action}",
|
||||
"shortcutUpdated": "Tastenkürzel aktualisiert für ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Automatisches Überspringen",
|
||||
"autoSkipIntro": "Intro automatisch überspringen",
|
||||
"autoSkipIntroDescription": "Intro-Marker nach wenigen Sekunden automatisch überspringen",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Download-Speicherort geändert",
|
||||
"downloadLocationReset": "Download-Speicherort auf Standard zurückgesetzt",
|
||||
"downloadLocationInvalid": "Ausgewählter Ordner ist nicht beschreibbar",
|
||||
"downloadLocationSelectError": "Ordnerauswahl fehlgeschlagen",
|
||||
"downloadLocationPickerUnavailable": "Die Ordnerauswahl ist auf diesem Gerät nicht verfügbar",
|
||||
"downloadOnWifiOnly": "Nur über WLAN herunterladen",
|
||||
"downloadOnWifiOnlyDescription": "Downloads über mobile Daten verhindern",
|
||||
"autoRemoveWatchedDownloads": "Gesehene Downloads automatisch entfernen",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Helligkeit",
|
||||
"hexColor": "Hex-Farbe",
|
||||
"expandText": "Text ausklappen",
|
||||
"collapseText": "Text einklappen"
|
||||
"collapseText": "Text einklappen",
|
||||
"alphabetNavigation": "Alphabetische Navigation",
|
||||
"alphabetScrollHint": "Nach oben oder unten wischen, um einen Buchstaben weiterzugehen",
|
||||
"rowColumnPosition": "Zeile ${row} von ${rowCount}, Spalte ${column} von ${columnCount}",
|
||||
"rowPosition": "Zeile ${row} von ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Zufallswiedergabe",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Als gesehen markieren",
|
||||
"markAsUnwatched": "Als ungesehen markieren"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Audiospur ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Untertitel",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Nächstes abspielen",
|
||||
"playButton": "Wiedergeben",
|
||||
"pauseButton": "Pause",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "${seconds} Sekunden zurück",
|
||||
"seekForwardButton": "${seconds} Sekunden vor",
|
||||
"previousButton": "Vorherige Episode",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.",
|
||||
"liveStreamInterrupted": "Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.",
|
||||
"fileInfoNotAvailable": "Dateiinfo nicht verfügbar",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Fehler beim Laden der Dateiinfo: ${error}",
|
||||
"errorLoadingSeries": "Fehler beim Laden der Serie",
|
||||
"musicNotSupported": "Musikwiedergabe wird noch nicht unterstützt",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Verbindung eines anderen Profils leihen. PIN-geschützte Profile erfordern eine PIN.",
|
||||
"borrowEmpty": "Noch nichts zum Ausleihen.",
|
||||
"borrowEmptySubtitle": "Verbinde zuerst Plex oder Jellyfin mit einem anderen Profil.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Von ${displayName}",
|
||||
"borrowConnectionBorrowed": "Verbindung ausgeliehen.",
|
||||
"borrowFailed": "Verbindung konnte nicht ausgeliehen werden.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoriten",
|
||||
"reorderFavorites": "Favoriten sortieren",
|
||||
"favoritesLoadFailed": "Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Laufender Sitzung beitreten",
|
||||
"watchFromStart": "Von Anfang an ansehen (vor ${minutes} Min.)",
|
||||
"watchLive": "Live ansehen",
|
||||
|
||||
+13
-4
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Export Settings",
|
||||
"exportSettingsDescription": "Save your preferences to a file",
|
||||
"exportSettingsSuccess": "Settings exported",
|
||||
"exportSettingsFailed": "Could not export settings",
|
||||
"importSettings": "Import Settings",
|
||||
"importSettingsDescription": "Restore preferences from a file",
|
||||
"importSettingsConfirm": "This will replace your current settings. Continue?",
|
||||
"importSettingsSuccess": "Settings imported",
|
||||
"importSettingsFailed": "Could not import settings",
|
||||
"importSettingsInvalidFile": "This file isn't a valid Plezy settings export",
|
||||
"importSettingsNoUser": "Sign in before importing settings",
|
||||
"shortcutsReset": "Shortcuts reset to defaults",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Duration must be between ${min} and ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Shortcut already assigned to ${action}",
|
||||
"shortcutUpdated": "Shortcut updated for ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Auto Skip",
|
||||
"autoSkipIntro": "Auto Skip Intro",
|
||||
"autoSkipIntroDescription": "Automatically skip intro markers after a few seconds",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Download location changed",
|
||||
"downloadLocationReset": "Download location reset to default",
|
||||
"downloadLocationInvalid": "Selected folder is not writable",
|
||||
"downloadLocationSelectError": "Failed to select folder",
|
||||
"downloadLocationPickerUnavailable": "Folder selection is not available on this device",
|
||||
"downloadOnWifiOnly": "Download on WiFi only",
|
||||
"downloadOnWifiOnlyDescription": "Prevent downloads when on cellular data",
|
||||
"autoRemoveWatchedDownloads": "Auto-remove watched downloads",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Brightness",
|
||||
"hexColor": "Hex color",
|
||||
"expandText": "Expand text",
|
||||
"collapseText": "Collapse text"
|
||||
"collapseText": "Collapse text",
|
||||
"alphabetNavigation": "Alphabet navigation",
|
||||
"alphabetScrollHint": "Swipe up or down to move by letter",
|
||||
"rowColumnPosition": "Row ${row} of ${rowCount}, column ${column} of ${columnCount}",
|
||||
"rowPosition": "Row ${row} of ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Shuffle play",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Mark as watched",
|
||||
"markAsUnwatched": "Mark as unwatched"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Audio Track ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Subtitles",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Play Next",
|
||||
"playButton": "Play",
|
||||
"pauseButton": "Pause",
|
||||
"showPlaybackControls": "Show playback controls",
|
||||
"hidePlaybackControls": "Hide playback controls",
|
||||
"seekBackwardButton": "Seek backward ${seconds} seconds",
|
||||
"seekForwardButton": "Seek forward ${seconds} seconds",
|
||||
"previousButton": "Previous episode",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Borrow another profile's connection. PIN-protected profiles require a PIN.",
|
||||
"borrowEmpty": "Nothing to borrow yet.",
|
||||
"borrowEmptySubtitle": "Connect Plex or Jellyfin to another profile first.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "From ${displayName}",
|
||||
"borrowConnectionBorrowed": "Connection borrowed.",
|
||||
"borrowFailed": "Failed to borrow connection.",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Esperando aprobación…",
|
||||
"quickConnectCancel": "Cancelar",
|
||||
"quickConnectExpired": "Quick Connect caducó. Inténtalo de nuevo.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancelar",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relay de Ver Juntos",
|
||||
"watchTogetherRelayDescription": "Define un relay personalizado. Todos deben usar el mismo servidor.",
|
||||
"watchTogetherRelayHint": "https://mi-relay.ejemplo.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Informes de Errores",
|
||||
"crashReportingDescription": "Enviar informes de errores para mejorar la aplicación",
|
||||
"debugLogging": "Registro de Depuración",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Exportar Configuración",
|
||||
"exportSettingsDescription": "Guardar tus preferencias en un archivo",
|
||||
"exportSettingsSuccess": "Configuración exportada",
|
||||
"exportSettingsFailed": "No se pudo exportar la configuración",
|
||||
"importSettings": "Importar Configuración",
|
||||
"importSettingsDescription": "Restaurar preferencias desde un archivo",
|
||||
"importSettingsConfirm": "Esto reemplazará tu configuración actual. ¿Continuar?",
|
||||
"importSettingsSuccess": "Configuración importada",
|
||||
"importSettingsFailed": "No se pudo importar la configuración",
|
||||
"importSettingsInvalidFile": "Este archivo no es una exportación válida de Plezy",
|
||||
"importSettingsNoUser": "Inicia sesión antes de importar la configuración",
|
||||
"shortcutsReset": "Atajos restablecidos a los valores predeterminados",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "La duración debe estar entre ${min} y ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "El atajo ya está asignado a ${action}",
|
||||
"shortcutUpdated": "Atajo actualizado para ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Salto automático",
|
||||
"autoSkipIntro": "Saltar Intro automáticamente",
|
||||
"autoSkipIntroDescription": "Saltar automáticamente los marcadores de intro después de unos segundos",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Ubicación de descarga cambiada",
|
||||
"downloadLocationReset": "Ubicación de descarga restablecida al predeterminado",
|
||||
"downloadLocationInvalid": "La carpeta seleccionada no tiene permisos de escritura",
|
||||
"downloadLocationSelectError": "Error al seleccionar la carpeta",
|
||||
"downloadLocationPickerUnavailable": "La selección de carpetas no está disponible en este dispositivo",
|
||||
"downloadOnWifiOnly": "Descargar solo con WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Evitar descargas cuando se usan datos móviles",
|
||||
"autoRemoveWatchedDownloads": "Eliminar descargas vistas automáticamente",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Brillo",
|
||||
"hexColor": "Color hexadecimal",
|
||||
"expandText": "Expandir texto",
|
||||
"collapseText": "Contraer texto"
|
||||
"collapseText": "Contraer texto",
|
||||
"alphabetNavigation": "Navegación alfabética",
|
||||
"alphabetScrollHint": "Desliza hacia arriba o abajo para avanzar por letra",
|
||||
"rowColumnPosition": "Fila ${row} de ${rowCount}, columna ${column} de ${columnCount}",
|
||||
"rowPosition": "Fila ${row} de ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Reproducción aleatoria",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Marcar como visto",
|
||||
"markAsUnwatched": "Marcar como no visto"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Pista de audio ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Subtítulos",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Reproducir siguiente",
|
||||
"playButton": "Reproducir",
|
||||
"pauseButton": "Pausa",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Retroceder ${seconds} segundos",
|
||||
"seekForwardButton": "Avanzar ${seconds} segundos",
|
||||
"previousButton": "Episodio anterior",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.",
|
||||
"liveStreamInterrupted": "La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.",
|
||||
"fileInfoNotAvailable": "Información de archivo no disponible",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Error al cargar info de archivo: ${error}",
|
||||
"errorLoadingSeries": "Error al cargar la serie",
|
||||
"musicNotSupported": "La reproducción de música aún no está soportada",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Toma prestada la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.",
|
||||
"borrowEmpty": "Nada para tomar prestado todavía.",
|
||||
"borrowEmptySubtitle": "Conecta Plex o Jellyfin primero a otro perfil.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "De ${displayName}",
|
||||
"borrowConnectionBorrowed": "Conexión tomada prestada.",
|
||||
"borrowFailed": "No se pudo tomar prestada la conexión.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos",
|
||||
"favoritesLoadFailed": "No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Unirse a sesión en curso",
|
||||
"watchFromStart": "Ver desde el inicio (hace ${minutes} min)",
|
||||
"watchLive": "Ver en vivo",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "En attente d'approbation…",
|
||||
"quickConnectCancel": "Annuler",
|
||||
"quickConnectExpired": "Quick Connect a expiré. Réessayez.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuler",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relais Regarder Ensemble",
|
||||
"watchTogetherRelayDescription": "Définir un relay personnalisé. Tout le monde doit utiliser le même serveur.",
|
||||
"watchTogetherRelayHint": "https://mon-relais.exemple.fr",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Rapports de plantage",
|
||||
"crashReportingDescription": "Envoyer des rapports de plantage pour améliorer l'application",
|
||||
"debugLogging": "Journalisation de débogage",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Exporter les paramètres",
|
||||
"exportSettingsDescription": "Enregistrer vos préférences dans un fichier",
|
||||
"exportSettingsSuccess": "Paramètres exportés",
|
||||
"exportSettingsFailed": "Impossible d'exporter les paramètres",
|
||||
"importSettings": "Importer les paramètres",
|
||||
"importSettingsDescription": "Restaurer les préférences depuis un fichier",
|
||||
"importSettingsConfirm": "Cela remplacera vos paramètres actuels. Continuer ?",
|
||||
"importSettingsSuccess": "Paramètres importés",
|
||||
"importSettingsFailed": "Impossible d'importer les paramètres",
|
||||
"importSettingsInvalidFile": "Ce fichier n'est pas un export Plezy valide",
|
||||
"importSettingsNoUser": "Connectez-vous avant d'importer les paramètres",
|
||||
"shortcutsReset": "Raccourcis réinitialisés aux valeurs par défaut",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "La durée doit être comprise entre ${min} et ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Raccourci déjà attribué à ${action}",
|
||||
"shortcutUpdated": "Raccourci mis à jour pour ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Skip automatique",
|
||||
"autoSkipIntro": "Skip automatique de l'introduction",
|
||||
"autoSkipIntroDescription": "Skipper automatiquement l'introduction après quelques secondes",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Emplacement de téléchargement modifié",
|
||||
"downloadLocationReset": "Emplacement de téléchargement réinitialisé à la valeur par défaut",
|
||||
"downloadLocationInvalid": "Le dossier sélectionné n'est pas accessible en écriture",
|
||||
"downloadLocationSelectError": "Échec de la sélection du dossier",
|
||||
"downloadLocationPickerUnavailable": "La sélection de dossier n’est pas disponible sur cet appareil",
|
||||
"downloadOnWifiOnly": "Télécharger uniquement via WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Empêcher les téléchargements lorsque vous utilisez les données cellulaires",
|
||||
"autoRemoveWatchedDownloads": "Supprimer automatiquement les téléchargements vus",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Luminosité",
|
||||
"hexColor": "Couleur hexadécimale",
|
||||
"expandText": "Développer le texte",
|
||||
"collapseText": "Replier le texte"
|
||||
"collapseText": "Replier le texte",
|
||||
"alphabetNavigation": "Navigation alphabétique",
|
||||
"alphabetScrollHint": "Balayez vers le haut ou le bas pour changer de lettre",
|
||||
"rowColumnPosition": "Ligne ${row} sur ${rowCount}, colonne ${column} sur ${columnCount}",
|
||||
"rowPosition": "Ligne ${row} sur ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Lecture aléatoire",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Marqué comme vu",
|
||||
"markAsUnwatched": "Marqué comme non vu"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Piste audio ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Sous-titres",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Lire l'épisode suivant",
|
||||
"playButton": "Lire",
|
||||
"pauseButton": "Pause",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Reculer de ${seconds} secondes",
|
||||
"seekForwardButton": "Avancer de ${seconds} secondes",
|
||||
"previousButton": "Épisode précédent",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.",
|
||||
"liveStreamInterrupted": "Le direct a été interrompu. Appuyez sur Lecture pour réessayer.",
|
||||
"fileInfoNotAvailable": "Informations sur le fichier non disponibles",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Erreur lors du chargement des informations sur le fichier: ${error}",
|
||||
"errorLoadingSeries": "Erreur lors du chargement de la série",
|
||||
"musicNotSupported": "La lecture de musique n'est pas encore prise en charge",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Emprunter la connexion d'un autre profil. Les profils protégés par PIN exigent un PIN.",
|
||||
"borrowEmpty": "Rien à emprunter pour le moment.",
|
||||
"borrowEmptySubtitle": "Connectez d'abord Plex ou Jellyfin à un autre profil.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "De ${displayName}",
|
||||
"borrowConnectionBorrowed": "Connexion empruntée.",
|
||||
"borrowFailed": "Impossible d'emprunter la connexion.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoris",
|
||||
"reorderFavorites": "Réorganiser les favoris",
|
||||
"favoritesLoadFailed": "Impossible de charger les favoris. Vérifiez votre connexion et réessayez.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Rejoindre la session en cours",
|
||||
"watchFromStart": "Regarder depuis le début (il y a ${minutes} min)",
|
||||
"watchLive": "Regarder en direct",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "In attesa di approvazione…",
|
||||
"quickConnectCancel": "Annulla",
|
||||
"quickConnectExpired": "Quick Connect scaduto. Riprova.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancella",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relay Guarda Insieme",
|
||||
"watchTogetherRelayDescription": "Imposta un relay personalizzato. Tutti devono usare lo stesso server.",
|
||||
"watchTogetherRelayHint": "https://mio-relay.esempio.it",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Segnalazione errori",
|
||||
"crashReportingDescription": "Invia segnalazioni di errori per migliorare l'app",
|
||||
"debugLogging": "Log di debug",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Esporta impostazioni",
|
||||
"exportSettingsDescription": "Salva le tue preferenze in un file",
|
||||
"exportSettingsSuccess": "Impostazioni esportate",
|
||||
"exportSettingsFailed": "Impossibile esportare le impostazioni",
|
||||
"importSettings": "Importa impostazioni",
|
||||
"importSettingsDescription": "Ripristina le preferenze da un file",
|
||||
"importSettingsConfirm": "Questa azione sostituirà le impostazioni attuali. Continuare?",
|
||||
"importSettingsSuccess": "Impostazioni importate",
|
||||
"importSettingsFailed": "Impossibile importare le impostazioni",
|
||||
"importSettingsInvalidFile": "Questo file non è un'esportazione Plezy valida",
|
||||
"importSettingsNoUser": "Accedi prima di importare le impostazioni",
|
||||
"shortcutsReset": "Scorciatoie ripristinate alle impostazioni predefinite",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "la durata deve essere compresa tra ${min} e ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Scorciatoia già assegnata a ${action}",
|
||||
"shortcutUpdated": "Scorciatoia aggiornata per ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Salto Automatico",
|
||||
"autoSkipIntro": "Salta Intro Automaticamente",
|
||||
"autoSkipIntroDescription": "Salta automaticamente i marcatori dell'intro dopo alcuni secondi",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Posizione di download modificata",
|
||||
"downloadLocationReset": "Posizione di download ripristinata a predefinita",
|
||||
"downloadLocationInvalid": "La cartella selezionata non è scrivibile",
|
||||
"downloadLocationSelectError": "Impossibile selezionare la cartella",
|
||||
"downloadLocationPickerUnavailable": "La selezione della cartella non è disponibile su questo dispositivo",
|
||||
"downloadOnWifiOnly": "Scarica solo con WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Impedisci i download quando si utilizza la rete dati cellulare",
|
||||
"autoRemoveWatchedDownloads": "Rimuovi automaticamente i download visti",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Luminosità",
|
||||
"hexColor": "Colore esadecimale",
|
||||
"expandText": "Espandi il testo",
|
||||
"collapseText": "Comprimi il testo"
|
||||
"collapseText": "Comprimi il testo",
|
||||
"alphabetNavigation": "Navigazione alfabetica",
|
||||
"alphabetScrollHint": "Scorri verso l'alto o il basso per cambiare lettera",
|
||||
"rowColumnPosition": "Riga ${row} di ${rowCount}, colonna ${column} di ${columnCount}",
|
||||
"rowPosition": "Riga ${row} di ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Riproduzione casuale",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Segna come visto",
|
||||
"markAsUnwatched": "Segna come non visto"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Traccia audio ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Sottotitoli",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Riproduci successivo",
|
||||
"playButton": "Riproduci",
|
||||
"pauseButton": "Pausa",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Riavvolgi di ${seconds} secondi",
|
||||
"seekForwardButton": "Avanza di ${seconds} secondi",
|
||||
"previousButton": "Episodio precedente",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.",
|
||||
"liveStreamInterrupted": "La diretta si è interrotta. Premi Riproduci per riprovare.",
|
||||
"fileInfoNotAvailable": "Informazioni sul file non disponibili",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Errore caricamento informazioni sul file: ${error}",
|
||||
"errorLoadingSeries": "Errore caricamento serie",
|
||||
"musicNotSupported": "La riproduzione musicale non è ancora supportata",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Prendi in prestito la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.",
|
||||
"borrowEmpty": "Nulla da prendere in prestito al momento.",
|
||||
"borrowEmptySubtitle": "Collega prima Plex o Jellyfin a un altro profilo.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Da ${displayName}",
|
||||
"borrowConnectionBorrowed": "Connessione presa in prestito.",
|
||||
"borrowFailed": "Impossibile prendere in prestito la connessione.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Preferiti",
|
||||
"reorderFavorites": "Riordina preferiti",
|
||||
"favoritesLoadFailed": "Impossibile caricare i preferiti. Controlla la connessione e riprova.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Partecipa alla sessione in corso",
|
||||
"watchFromStart": "Guarda dall'inizio (${minutes} min fa)",
|
||||
"watchLive": "Guarda in diretta",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "承認を待っています…",
|
||||
"quickConnectCancel": "キャンセル",
|
||||
"quickConnectExpired": "Quick Connectの有効期限が切れました。もう一度お試しください。",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "キャンセル",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "一緒に視聴リレーサーバー",
|
||||
"watchTogetherRelayDescription": "カスタムリレーを設定します。全員が同じサーバーを使う必要があります。",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "クラッシュレポート",
|
||||
"crashReportingDescription": "アプリの改善に役立つクラッシュレポートを送信",
|
||||
"debugLogging": "デバッグログ",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "設定をエクスポート",
|
||||
"exportSettingsDescription": "設定をファイルに保存",
|
||||
"exportSettingsSuccess": "設定をエクスポートしました",
|
||||
"exportSettingsFailed": "設定をエクスポートできませんでした",
|
||||
"importSettings": "設定をインポート",
|
||||
"importSettingsDescription": "ファイルから設定を復元",
|
||||
"importSettingsConfirm": "現在の設定を置き換えます。続行しますか?",
|
||||
"importSettingsSuccess": "設定をインポートしました",
|
||||
"importSettingsFailed": "設定をインポートできませんでした",
|
||||
"importSettingsInvalidFile": "このファイルは有効なPlezyの設定エクスポートではありません",
|
||||
"importSettingsNoUser": "設定をインポートする前にサインインしてください",
|
||||
"shortcutsReset": "ショートカットをデフォルトにリセットしました",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "時間は${min}から${max} ${unit}の間である必要があります",
|
||||
"shortcutAlreadyAssigned": "ショートカットは既に${action}に割り当てられています",
|
||||
"shortcutUpdated": "${action}のショートカットを更新しました",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "自動スキップ",
|
||||
"autoSkipIntro": "イントロを自動スキップ",
|
||||
"autoSkipIntroDescription": "数秒後にイントロマーカーを自動的にスキップ",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "ダウンロード場所を変更しました",
|
||||
"downloadLocationReset": "ダウンロード場所をデフォルトにリセットしました",
|
||||
"downloadLocationInvalid": "選択したフォルダは書き込みできません",
|
||||
"downloadLocationSelectError": "フォルダの選択に失敗しました",
|
||||
"downloadLocationPickerUnavailable": "このデバイスではフォルダを選択できません",
|
||||
"downloadOnWifiOnly": "WiFiのみでダウンロード",
|
||||
"downloadOnWifiOnlyDescription": "モバイルデータ通信時のダウンロードを防止",
|
||||
"autoRemoveWatchedDownloads": "視聴済みダウンロードの自動削除",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "明るさ",
|
||||
"hexColor": "16進カラー",
|
||||
"expandText": "テキストを展開",
|
||||
"collapseText": "テキストを折りたたむ"
|
||||
"collapseText": "テキストを折りたたむ",
|
||||
"alphabetNavigation": "アルファベットナビゲーション",
|
||||
"alphabetScrollHint": "上下にスワイプして文字ごとに移動",
|
||||
"rowColumnPosition": "${rowCount}行中${row}行、${columnCount}列中${column}列",
|
||||
"rowPosition": "${rowCount}行中${row}行"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "シャッフル再生",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "視聴済みにする",
|
||||
"markAsUnwatched": "未視聴にする"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "音声トラック${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "音声",
|
||||
"subtitlesLabel": "字幕",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "次を再生",
|
||||
"playButton": "再生",
|
||||
"pauseButton": "一時停止",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "${seconds}秒戻る",
|
||||
"seekForwardButton": "${seconds}秒進む",
|
||||
"previousButton": "前のエピソード",
|
||||
@@ -552,11 +560,11 @@
|
||||
"streamInterrupted": "ストリームが中断されました。再生を押すかシークして再試行してください。",
|
||||
"liveStreamInterrupted": "ライブストリームが中断されました。再生を押して再試行してください。",
|
||||
"fileInfoNotAvailable": "ファイル情報が利用できません",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "ファイル情報の読み込みエラー: ${error}",
|
||||
"errorLoadingSeries": "シリーズの読み込みエラー",
|
||||
"musicNotSupported": "音楽の再生はまだサポートされていません",
|
||||
@@ -679,6 +687,7 @@
|
||||
"borrowExplain": "別のプロフィールの接続を借用します。PIN保護されたプロフィールにはPINが必要です。",
|
||||
"borrowEmpty": "まだ借りるものがありません。",
|
||||
"borrowEmptySubtitle": "まず別のプロフィールにPlexまたはJellyfinを接続してください。",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "${displayName}から",
|
||||
"borrowConnectionBorrowed": "接続を借用しました。",
|
||||
"borrowFailed": "接続を借用できませんでした。",
|
||||
@@ -942,7 +951,7 @@
|
||||
"favorites": "お気に入り",
|
||||
"reorderFavorites": "お気に入りを並べ替え",
|
||||
"favoritesLoadFailed": "お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "進行中のセッションに参加",
|
||||
"watchFromStart": "最初から視聴(${minutes}分前に開始)",
|
||||
"watchLive": "ライブで視聴",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "승인 대기 중…",
|
||||
"quickConnectCancel": "취소",
|
||||
"quickConnectExpired": "Quick Connect가 만료되었습니다. 다시 시도하세요.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "취소",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "함께 보기 릴레이",
|
||||
"watchTogetherRelayDescription": "사용자 지정 릴레이를 설정합니다. 모두 같은 서버를 사용해야 합니다.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "충돌 보고",
|
||||
"crashReportingDescription": "앱 개선을 위해 충돌 보고서 전송",
|
||||
"debugLogging": "디버그 로깅",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "설정 내보내기",
|
||||
"exportSettingsDescription": "기본 설정을 파일로 저장",
|
||||
"exportSettingsSuccess": "설정 내보내기 완료",
|
||||
"exportSettingsFailed": "설정을 내보낼 수 없습니다",
|
||||
"importSettings": "설정 가져오기",
|
||||
"importSettingsDescription": "파일에서 기본 설정 복원",
|
||||
"importSettingsConfirm": "현재 설정을 대체합니다. 계속하시겠습니까?",
|
||||
"importSettingsSuccess": "설정 가져오기 완료",
|
||||
"importSettingsFailed": "설정을 가져올 수 없습니다",
|
||||
"importSettingsInvalidFile": "유효한 Plezy 설정 내보내기 파일이 아닙니다",
|
||||
"importSettingsNoUser": "설정을 가져오기 전에 로그인하세요",
|
||||
"shortcutsReset": "단축키가 기본값으로 재설정되었습니다",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "기간은 ${min}과 ${max} ${unit} 사이여야 합니다",
|
||||
"shortcutAlreadyAssigned": "단축키가 이미 ${action}에 할당 되었습니다",
|
||||
"shortcutUpdated": "단축키가 ${action}에 대해 업데이트 되었습니다",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "자동 건너뛰기",
|
||||
"autoSkipIntro": "자동으로 오프닝 건너뛰기",
|
||||
"autoSkipIntroDescription": "몇 초 후 오프닝을 자동으로 건너뛰기",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "다운로드 위치가 변경 되었습니다",
|
||||
"downloadLocationReset": "다운로드 위치가 기본값으로 재설정 되었습니다",
|
||||
"downloadLocationInvalid": "선택한 폴더에 쓰기 권한이 없습니다",
|
||||
"downloadLocationSelectError": "폴더 선택 실패",
|
||||
"downloadLocationPickerUnavailable": "이 기기에서는 폴더를 선택할 수 없습니다",
|
||||
"downloadOnWifiOnly": "WiFi 연결 시에만 다운로드",
|
||||
"downloadOnWifiOnlyDescription": "셀룰러 데이터 사용 시 다운로드 불가",
|
||||
"autoRemoveWatchedDownloads": "시청한 다운로드 자동 삭제",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "밝기",
|
||||
"hexColor": "16진수 색상",
|
||||
"expandText": "텍스트 펼치기",
|
||||
"collapseText": "텍스트 접기"
|
||||
"collapseText": "텍스트 접기",
|
||||
"alphabetNavigation": "알파벳 탐색",
|
||||
"alphabetScrollHint": "위아래로 스와이프하여 글자별로 이동",
|
||||
"rowColumnPosition": "${rowCount}행 중 ${row}행, ${columnCount}열 중 ${column}열",
|
||||
"rowPosition": "${rowCount}행 중 ${row}행"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "무작위 재생",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "시청 완료로 표시",
|
||||
"markAsUnwatched": "시청 안 함으로 표시"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "오디오 트랙 ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "오디오",
|
||||
"subtitlesLabel": "자막",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "다음 재생",
|
||||
"playButton": "재생",
|
||||
"pauseButton": "일시정지",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "${seconds} 초 뒤로",
|
||||
"seekForwardButton": "${seconds} 초 앞으로",
|
||||
"previousButton": "이전 에피소드",
|
||||
@@ -552,11 +560,11 @@
|
||||
"streamInterrupted": "스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.",
|
||||
"liveStreamInterrupted": "라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.",
|
||||
"fileInfoNotAvailable": "파일 정보가 없습니다",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "파일 정보 로딩 중 오류: ${error}",
|
||||
"errorLoadingSeries": "시리즈 로딩 중 오류",
|
||||
"musicNotSupported": "음악 재생 미지원",
|
||||
@@ -679,6 +687,7 @@
|
||||
"borrowExplain": "다른 프로필의 연결을 빌립니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.",
|
||||
"borrowEmpty": "아직 빌릴 것이 없습니다.",
|
||||
"borrowEmptySubtitle": "먼저 다른 프로필에 Plex 또는 Jellyfin을 연결하세요.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "${displayName}에서",
|
||||
"borrowConnectionBorrowed": "연결을 빌렸습니다.",
|
||||
"borrowFailed": "연결을 빌리지 못했습니다.",
|
||||
@@ -942,7 +951,7 @@
|
||||
"favorites": "즐겨찾기",
|
||||
"reorderFavorites": "즐겨찾기 순서 변경",
|
||||
"favoritesLoadFailed": "즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "진행 중인 세션 참여",
|
||||
"watchFromStart": "처음부터 시청 (${minutes}분 전 시작)",
|
||||
"watchLive": "실시간 시청",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Venter på godkjenning…",
|
||||
"quickConnectCancel": "Avbryt",
|
||||
"quickConnectExpired": "Quick Connect er utløpt. Prøv igjen.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Avbryt",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Se Sammen-relay",
|
||||
"watchTogetherRelayDescription": "Angi en egendefinert relay. Alle må bruke samme server.",
|
||||
"watchTogetherRelayHint": "https://min-relay.eksempel.no",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Krasjrapportering",
|
||||
"crashReportingDescription": "Send krasjrapporter for å hjelpe med å forbedre appen",
|
||||
"debugLogging": "Feilsøkingslogging",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Eksporter innstillinger",
|
||||
"exportSettingsDescription": "Lagre innstillingene i en fil",
|
||||
"exportSettingsSuccess": "Innstillinger eksportert",
|
||||
"exportSettingsFailed": "Kunne ikke eksportere innstillinger",
|
||||
"importSettings": "Importer innstillinger",
|
||||
"importSettingsDescription": "Gjenopprett innstillinger fra en fil",
|
||||
"importSettingsConfirm": "Dette vil erstatte nåværende innstillinger. Fortsette?",
|
||||
"importSettingsSuccess": "Innstillinger importert",
|
||||
"importSettingsFailed": "Kunne ikke importere innstillinger",
|
||||
"importSettingsInvalidFile": "Denne filen er ikke en gyldig Plezy-innstillingseksport",
|
||||
"importSettingsNoUser": "Logg inn før import av innstillinger",
|
||||
"shortcutsReset": "Snarveier tilbakestilt til standard",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Varigheten må være mellom ${min} og ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Snarvei allerede tilordnet til ${action}",
|
||||
"shortcutUpdated": "Snarvei oppdatert for ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Automatisk hopp",
|
||||
"autoSkipIntro": "Hopp over intro automatisk",
|
||||
"autoSkipIntroDescription": "Hopp automatisk over intromarkører etter noen sekunder",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Nedlastingsplassering endret",
|
||||
"downloadLocationReset": "Nedlastingsplassering tilbakestilt til standard",
|
||||
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
|
||||
"downloadLocationSelectError": "Kunne ikke velge mappe",
|
||||
"downloadLocationPickerUnavailable": "Mappevalg er ikke tilgjengelig på denne enheten",
|
||||
"downloadOnWifiOnly": "Last ned kun på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Forhindre nedlastinger på mobildata",
|
||||
"autoRemoveWatchedDownloads": "Fjern sette nedlastinger automatisk",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Lysstyrke",
|
||||
"hexColor": "Heksadesimal farge",
|
||||
"expandText": "Utvid tekst",
|
||||
"collapseText": "Fold sammen tekst"
|
||||
"collapseText": "Fold sammen tekst",
|
||||
"alphabetNavigation": "Alfabetisk navigasjon",
|
||||
"alphabetScrollHint": "Sveip opp eller ned for å flytte én bokstav",
|
||||
"rowColumnPosition": "Rad ${row} av ${rowCount}, kolonne ${column} av ${columnCount}",
|
||||
"rowPosition": "Rad ${row} av ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Tilfeldig avspilling",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Merk som sett",
|
||||
"markAsUnwatched": "Merk som usett"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Lydspor ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Lyd",
|
||||
"subtitlesLabel": "Undertekster",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Spill neste",
|
||||
"playButton": "Spill av",
|
||||
"pauseButton": "Pause",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Spol tilbake ${seconds} sekunder",
|
||||
"seekForwardButton": "Spol fremover ${seconds} sekunder",
|
||||
"previousButton": "Forrige episode",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.",
|
||||
"liveStreamInterrupted": "Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.",
|
||||
"fileInfoNotAvailable": "Filinformasjon ikke tilgjengelig",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Feil ved lasting av filinformasjon: ${error}",
|
||||
"errorLoadingSeries": "Feil ved lasting av serie",
|
||||
"musicNotSupported": "Musikkavspilling støttes ikke ennå",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Lån en annen profils tilkobling. PIN-beskyttede profiler krever PIN.",
|
||||
"borrowEmpty": "Ingenting å låne enda.",
|
||||
"borrowEmptySubtitle": "Koble Plex eller Jellyfin til en annen profil først.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Fra ${displayName}",
|
||||
"borrowConnectionBorrowed": "Tilkobling lånt.",
|
||||
"borrowFailed": "Kunne ikke låne tilkoblingen.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Endre rekkefølge på favoritter",
|
||||
"favoritesLoadFailed": "Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Bli med i pågående økt",
|
||||
"watchFromStart": "Se fra starten (${minutes} min siden)",
|
||||
"watchLive": "Se direkte",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Wachten op goedkeuring…",
|
||||
"quickConnectCancel": "Annuleren",
|
||||
"quickConnectExpired": "Quick Connect is verlopen. Probeer opnieuw.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuleren",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Samen Kijken Relay",
|
||||
"watchTogetherRelayDescription": "Stel een aangepaste relay in. Iedereen moet dezelfde server gebruiken.",
|
||||
"watchTogetherRelayHint": "https://mijn-relay.voorbeeld.nl",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Crashrapportage",
|
||||
"crashReportingDescription": "Crashrapporten verzenden om de app te verbeteren",
|
||||
"debugLogging": "Debug logging",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Instellingen exporteren",
|
||||
"exportSettingsDescription": "Sla je voorkeuren op in een bestand",
|
||||
"exportSettingsSuccess": "Instellingen geëxporteerd",
|
||||
"exportSettingsFailed": "Kon instellingen niet exporteren",
|
||||
"importSettings": "Instellingen importeren",
|
||||
"importSettingsDescription": "Voorkeuren herstellen vanuit een bestand",
|
||||
"importSettingsConfirm": "Hiermee worden je huidige instellingen vervangen. Doorgaan?",
|
||||
"importSettingsSuccess": "Instellingen geïmporteerd",
|
||||
"importSettingsFailed": "Kon instellingen niet importeren",
|
||||
"importSettingsInvalidFile": "Dit bestand is geen geldige Plezy-export",
|
||||
"importSettingsNoUser": "Meld je aan voordat je instellingen importeert",
|
||||
"shortcutsReset": "Sneltoetsen gereset naar standaard",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Duur moet tussen ${min} en ${max} ${unit} zijn",
|
||||
"shortcutAlreadyAssigned": "Sneltoets al toegewezen aan ${action}",
|
||||
"shortcutUpdated": "Sneltoets bijgewerkt voor ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Automatisch Overslaan",
|
||||
"autoSkipIntro": "Intro Automatisch Overslaan",
|
||||
"autoSkipIntroDescription": "Intro-markeringen na enkele seconden automatisch overslaan",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Downloadlocatie gewijzigd",
|
||||
"downloadLocationReset": "Downloadlocatie hersteld naar standaard",
|
||||
"downloadLocationInvalid": "Geselecteerde map is niet beschrijfbaar",
|
||||
"downloadLocationSelectError": "Kan map niet selecteren",
|
||||
"downloadLocationPickerUnavailable": "Mapselectie is niet beschikbaar op dit apparaat",
|
||||
"downloadOnWifiOnly": "Alleen via WiFi downloaden",
|
||||
"downloadOnWifiOnlyDescription": "Voorkom downloads bij gebruik van mobiele data",
|
||||
"autoRemoveWatchedDownloads": "Bekeken downloads automatisch verwijderen",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Helderheid",
|
||||
"hexColor": "Hexkleur",
|
||||
"expandText": "Tekst uitvouwen",
|
||||
"collapseText": "Tekst samenvouwen"
|
||||
"collapseText": "Tekst samenvouwen",
|
||||
"alphabetNavigation": "Alfabetische navigatie",
|
||||
"alphabetScrollHint": "Veeg omhoog of omlaag om per letter te bewegen",
|
||||
"rowColumnPosition": "Rij ${row} van ${rowCount}, kolom ${column} van ${columnCount}",
|
||||
"rowPosition": "Rij ${row} van ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Willekeurig afspelen",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Markeer als gekeken",
|
||||
"markAsUnwatched": "Markeer als ongekeken"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Audiospoor ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Ondertitels",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Volgende afspelen",
|
||||
"playButton": "Afspelen",
|
||||
"pauseButton": "Pauzeren",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Terugspoelen ${seconds} seconden",
|
||||
"seekForwardButton": "Vooruitspoelen ${seconds} seconden",
|
||||
"previousButton": "Vorige aflevering",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.",
|
||||
"liveStreamInterrupted": "De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.",
|
||||
"fileInfoNotAvailable": "Bestand informatie niet beschikbaar",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Fout bij laden bestand info: ${error}",
|
||||
"errorLoadingSeries": "Fout bij laden serie",
|
||||
"musicNotSupported": "Muziek afspelen wordt nog niet ondersteund",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Leen de verbinding van een ander profiel. PIN-beveiligde profielen vereisen een PIN.",
|
||||
"borrowEmpty": "Nog niets te lenen.",
|
||||
"borrowEmptySubtitle": "Verbind Plex of Jellyfin eerst met een ander profiel.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Van ${displayName}",
|
||||
"borrowConnectionBorrowed": "Verbinding geleend.",
|
||||
"borrowFailed": "Kan verbinding niet lenen.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favorieten",
|
||||
"reorderFavorites": "Favorieten herordenen",
|
||||
"favoritesLoadFailed": "Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Deelnemen aan lopende sessie",
|
||||
"watchFromStart": "Kijk vanaf het begin (${minutes} min geleden)",
|
||||
"watchLive": "Live kijken",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Oczekiwanie na zatwierdzenie…",
|
||||
"quickConnectCancel": "Anuluj",
|
||||
"quickConnectExpired": "Quick Connect wygasł. Spróbuj ponownie.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Anuluj",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relay Oglądaj Razem",
|
||||
"watchTogetherRelayDescription": "Ustaw własny relay. Wszyscy muszą używać tego samego serwera.",
|
||||
"watchTogetherRelayHint": "https://moj-relay.przyklad.pl",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Raportowanie błędów",
|
||||
"crashReportingDescription": "Wysyłaj raporty o błędach, aby pomóc ulepszyć aplikację",
|
||||
"debugLogging": "Logowanie debugowania",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Eksportuj ustawienia",
|
||||
"exportSettingsDescription": "Zapisz swoje preferencje do pliku",
|
||||
"exportSettingsSuccess": "Ustawienia wyeksportowane",
|
||||
"exportSettingsFailed": "Nie można wyeksportować ustawień",
|
||||
"importSettings": "Importuj ustawienia",
|
||||
"importSettingsDescription": "Przywróć preferencje z pliku",
|
||||
"importSettingsConfirm": "Bieżące ustawienia zostaną zastąpione. Kontynuować?",
|
||||
"importSettingsSuccess": "Ustawienia zaimportowane",
|
||||
"importSettingsFailed": "Nie można zaimportować ustawień",
|
||||
"importSettingsInvalidFile": "Ten plik nie jest prawidłowym eksportem Plezy",
|
||||
"importSettingsNoUser": "Zaloguj się przed importem ustawień",
|
||||
"shortcutsReset": "Skróty przywrócone do domyślnych",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Czas musi być między ${min} a ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Skrót jest już przypisany do ${action}",
|
||||
"shortcutUpdated": "Skrót zaktualizowany dla ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Automatyczne pomijanie",
|
||||
"autoSkipIntro": "Automatyczne pomijanie intro",
|
||||
"autoSkipIntroDescription": "Automatycznie pomijaj znaczniki intro po kilku sekundach",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Lokalizacja pobierania zmieniona",
|
||||
"downloadLocationReset": "Lokalizacja pobierania przywrócona do domyślnej",
|
||||
"downloadLocationInvalid": "Wybrany folder nie jest zapisywalny",
|
||||
"downloadLocationSelectError": "Nie udało się wybrać folderu",
|
||||
"downloadLocationPickerUnavailable": "Wybór folderu nie jest dostępny na tym urządzeniu",
|
||||
"downloadOnWifiOnly": "Pobieraj tylko przez WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Blokuj pobieranie na danych komórkowych",
|
||||
"autoRemoveWatchedDownloads": "Automatycznie usuwaj obejrzane pobrania",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Jasność",
|
||||
"hexColor": "Kolor szesnastkowy",
|
||||
"expandText": "Rozwiń tekst",
|
||||
"collapseText": "Zwiń tekst"
|
||||
"collapseText": "Zwiń tekst",
|
||||
"alphabetNavigation": "Nawigacja alfabetyczna",
|
||||
"alphabetScrollHint": "Przesuń w górę lub w dół, aby przejść o literę",
|
||||
"rowColumnPosition": "Wiersz ${row} z ${rowCount}, kolumna ${column} z ${columnCount}",
|
||||
"rowPosition": "Wiersz ${row} z ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Odtwarzanie losowe",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Oznacz jako obejrzane",
|
||||
"markAsUnwatched": "Oznacz jako nieobejrzane"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Ścieżka audio ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Napisy",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Odtwórz następny",
|
||||
"playButton": "Odtwórz",
|
||||
"pauseButton": "Pauza",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Przewiń do tyłu o ${seconds} sekund",
|
||||
"seekForwardButton": "Przewiń do przodu o ${seconds} sekund",
|
||||
"previousButton": "Poprzedni odcinek",
|
||||
@@ -555,11 +563,11 @@
|
||||
"streamInterrupted": "Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.",
|
||||
"liveStreamInterrupted": "Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.",
|
||||
"fileInfoNotAvailable": "Informacje o pliku niedostępne",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Błąd ładowania informacji o pliku: ${error}",
|
||||
"errorLoadingSeries": "Błąd ładowania serialu",
|
||||
"musicNotSupported": "Odtwarzanie muzyki nie jest jeszcze obsługiwane",
|
||||
@@ -682,6 +690,7 @@
|
||||
"borrowExplain": "Pożycz połączenie z innego profilu. Profile chronione PIN wymagają PIN-u.",
|
||||
"borrowEmpty": "Nic do pożyczenia.",
|
||||
"borrowEmptySubtitle": "Najpierw połącz Plex lub Jellyfin z innym profilem.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Od ${displayName}",
|
||||
"borrowConnectionBorrowed": "Połączenie pożyczone.",
|
||||
"borrowFailed": "Nie udało się pożyczyć połączenia.",
|
||||
@@ -948,7 +957,7 @@
|
||||
"favorites": "Ulubione",
|
||||
"reorderFavorites": "Zmień kolejność ulubionych",
|
||||
"favoritesLoadFailed": "Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Dołącz do trwającej sesji",
|
||||
"watchFromStart": "Oglądaj od początku (${minutes} min temu)",
|
||||
"watchLive": "Oglądaj na żywo",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "A aguardar aprovação…",
|
||||
"quickConnectCancel": "Cancelar",
|
||||
"quickConnectExpired": "Quick Connect expirou. Tente novamente.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancelar",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relay do Assistir Juntos",
|
||||
"watchTogetherRelayDescription": "Defina um relay personalizado. Todos devem usar o mesmo servidor.",
|
||||
"watchTogetherRelayHint": "https://meu-relay.exemplo.com.br",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Relatório de Erros",
|
||||
"crashReportingDescription": "Enviar relatórios de erros para ajudar a melhorar o app",
|
||||
"debugLogging": "Log de Depuração",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Exportar Configurações",
|
||||
"exportSettingsDescription": "Salve suas preferências em um arquivo",
|
||||
"exportSettingsSuccess": "Configurações exportadas",
|
||||
"exportSettingsFailed": "Não foi possível exportar as configurações",
|
||||
"importSettings": "Importar Configurações",
|
||||
"importSettingsDescription": "Restaurar preferências a partir de um arquivo",
|
||||
"importSettingsConfirm": "Isso substituirá suas configurações atuais. Continuar?",
|
||||
"importSettingsSuccess": "Configurações importadas",
|
||||
"importSettingsFailed": "Não foi possível importar as configurações",
|
||||
"importSettingsInvalidFile": "Este arquivo não é uma exportação válida do Plezy",
|
||||
"importSettingsNoUser": "Entre na conta antes de importar as configurações",
|
||||
"shortcutsReset": "Atalhos redefinidos para o padrão",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "A duração deve ser entre ${min} e ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Atalho já atribuído a ${action}",
|
||||
"shortcutUpdated": "Atalho atualizado para ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Pular Automaticamente",
|
||||
"autoSkipIntro": "Pular Intro Automaticamente",
|
||||
"autoSkipIntroDescription": "Pular marcadores de intro automaticamente após alguns segundos",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Local de download alterado",
|
||||
"downloadLocationReset": "Local de download redefinido para padrão",
|
||||
"downloadLocationInvalid": "A pasta selecionada não permite gravação",
|
||||
"downloadLocationSelectError": "Falha ao selecionar pasta",
|
||||
"downloadLocationPickerUnavailable": "A seleção de pasta não está disponível neste dispositivo",
|
||||
"downloadOnWifiOnly": "Baixar apenas no WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Impedir downloads quando em dados móveis",
|
||||
"autoRemoveWatchedDownloads": "Remover downloads assistidos automaticamente",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Brilho",
|
||||
"hexColor": "Cor hexadecimal",
|
||||
"expandText": "Expandir texto",
|
||||
"collapseText": "Recolher texto"
|
||||
"collapseText": "Recolher texto",
|
||||
"alphabetNavigation": "Navegação alfabética",
|
||||
"alphabetScrollHint": "Deslize para cima ou para baixo para avançar por letra",
|
||||
"rowColumnPosition": "Linha ${row} de ${rowCount}, coluna ${column} de ${columnCount}",
|
||||
"rowPosition": "Linha ${row} de ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Reprodução aleatória",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Marcar como assistido",
|
||||
"markAsUnwatched": "Marcar como não assistido"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Faixa de áudio ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Áudio",
|
||||
"subtitlesLabel": "Legendas",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Reproduzir Próximo",
|
||||
"playButton": "Reproduzir",
|
||||
"pauseButton": "Pausar",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Retroceder ${seconds} segundos",
|
||||
"seekForwardButton": "Avançar ${seconds} segundos",
|
||||
"previousButton": "Episódio anterior",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.",
|
||||
"liveStreamInterrupted": "A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.",
|
||||
"fileInfoNotAvailable": "Informações do arquivo não disponíveis",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Erro ao carregar info do arquivo: ${error}",
|
||||
"errorLoadingSeries": "Erro ao carregar série",
|
||||
"musicNotSupported": "Reprodução de música ainda não é suportada",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Use a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.",
|
||||
"borrowEmpty": "Nada para emprestar ainda.",
|
||||
"borrowEmptySubtitle": "Conecte Plex ou Jellyfin a outro perfil primeiro.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "De ${displayName}",
|
||||
"borrowConnectionBorrowed": "Conexão tomada emprestada.",
|
||||
"borrowFailed": "Não foi possível tomar a conexão emprestada.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos",
|
||||
"favoritesLoadFailed": "Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Entrar na sessão em andamento",
|
||||
"watchFromStart": "Assistir do início (${minutes} min atrás)",
|
||||
"watchLive": "Assistir ao vivo",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Ожидание подтверждения…",
|
||||
"quickConnectCancel": "Отмена",
|
||||
"quickConnectExpired": "Срок Quick Connect истек. Попробуйте снова.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Отмена",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Relay совместного просмотра",
|
||||
"watchTogetherRelayDescription": "Задайте свой relay. Все должны использовать один сервер.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Отчёты об ошибках",
|
||||
"crashReportingDescription": "Отправлять отчёты об ошибках для улучшения приложения",
|
||||
"debugLogging": "Журнал отладки",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Экспорт настроек",
|
||||
"exportSettingsDescription": "Сохранить настройки в файл",
|
||||
"exportSettingsSuccess": "Настройки экспортированы",
|
||||
"exportSettingsFailed": "Не удалось экспортировать настройки",
|
||||
"importSettings": "Импорт настроек",
|
||||
"importSettingsDescription": "Восстановить настройки из файла",
|
||||
"importSettingsConfirm": "Это заменит ваши текущие настройки. Продолжить?",
|
||||
"importSettingsSuccess": "Настройки импортированы",
|
||||
"importSettingsFailed": "Не удалось импортировать настройки",
|
||||
"importSettingsInvalidFile": "Этот файл не является действительным экспортом настроек Plezy",
|
||||
"importSettingsNoUser": "Войдите в систему перед импортом настроек",
|
||||
"shortcutsReset": "Горячие клавиши сброшены по умолчанию",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Длительность должна быть от ${min} до ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Клавиша уже назначена для ${action}",
|
||||
"shortcutUpdated": "Клавиша обновлена для ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Автопропуск",
|
||||
"autoSkipIntro": "Автопропуск вступления",
|
||||
"autoSkipIntroDescription": "Автоматически пропускать маркеры вступления через несколько секунд",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Место загрузки изменено",
|
||||
"downloadLocationReset": "Место загрузки сброшено по умолчанию",
|
||||
"downloadLocationInvalid": "Выбранная папка недоступна для записи",
|
||||
"downloadLocationSelectError": "Не удалось выбрать папку",
|
||||
"downloadLocationPickerUnavailable": "Выбор папки недоступен на этом устройстве",
|
||||
"downloadOnWifiOnly": "Загружать только по WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Запретить загрузку по мобильным данным",
|
||||
"autoRemoveWatchedDownloads": "Автоудаление просмотренных загрузок",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Яркость",
|
||||
"hexColor": "Шестнадцатеричный цвет",
|
||||
"expandText": "Развернуть текст",
|
||||
"collapseText": "Свернуть текст"
|
||||
"collapseText": "Свернуть текст",
|
||||
"alphabetNavigation": "Навигация по алфавиту",
|
||||
"alphabetScrollHint": "Проведите вверх или вниз для перехода по буквам",
|
||||
"rowColumnPosition": "Строка ${row} из ${rowCount}, столбец ${column} из ${columnCount}",
|
||||
"rowPosition": "Строка ${row} из ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Случайное воспроизведение",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Отметить как просмотренное",
|
||||
"markAsUnwatched": "Отметить как непросмотренное"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Аудиодорожка ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Аудио",
|
||||
"subtitlesLabel": "Субтитры",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Следующее",
|
||||
"playButton": "Воспроизвести",
|
||||
"pauseButton": "Пауза",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Перемотка назад на ${seconds} секунд",
|
||||
"seekForwardButton": "Перемотка вперёд на ${seconds} секунд",
|
||||
"previousButton": "Предыдущий эпизод",
|
||||
@@ -555,11 +563,11 @@
|
||||
"streamInterrupted": "Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.",
|
||||
"liveStreamInterrupted": "Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.",
|
||||
"fileInfoNotAvailable": "Информация о файле недоступна",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Ошибка загрузки информации о файле: ${error}",
|
||||
"errorLoadingSeries": "Ошибка загрузки сериала",
|
||||
"musicNotSupported": "Воспроизведение музыки пока не поддерживается",
|
||||
@@ -682,6 +690,7 @@
|
||||
"borrowExplain": "Заимствуйте подключение другого профиля. Для профилей с PIN нужен PIN.",
|
||||
"borrowEmpty": "Пока нечего заимствовать.",
|
||||
"borrowEmptySubtitle": "Сначала подключите Plex или Jellyfin к другому профилю.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Из ${displayName}",
|
||||
"borrowConnectionBorrowed": "Подключение заимствовано.",
|
||||
"borrowFailed": "Не удалось заимствовать подключение.",
|
||||
@@ -948,7 +957,7 @@
|
||||
"favorites": "Избранное",
|
||||
"reorderFavorites": "Изменить порядок избранного",
|
||||
"favoritesLoadFailed": "Не удалось загрузить избранное. Проверьте подключение и повторите попытку.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Присоединиться к текущему сеансу",
|
||||
"watchFromStart": "Смотреть сначала (${minutes} мин. назад)",
|
||||
"watchLive": "Смотреть в прямом эфире",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 16
|
||||
/// Strings: 23027 (1439 per locale)
|
||||
/// Strings: 23082 (1442 per locale)
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsBg extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetBg rateSheet = _TranslationsRateSheetBg._(_root);
|
||||
@override late final _TranslationsAccessibilityBg accessibility = _TranslationsAccessibilityBg._(_root);
|
||||
@override late final _TranslationsTooltipsBg tooltips = _TranslationsTooltipsBg._(_root);
|
||||
@override late final _TranslationsAudioTracksBg audioTracks = _TranslationsAudioTracksBg._(_root);
|
||||
@override late final _TranslationsVideoControlsBg videoControls = _TranslationsVideoControlsBg._(_root);
|
||||
@override late final _TranslationsMessagesBg messages = _TranslationsMessagesBg._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingBg subtitlingStyling = _TranslationsSubtitlingStylingBg._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthBg extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Изчакване на одобрение…';
|
||||
@override String get quickConnectCancel => 'Отказ';
|
||||
@override String get quickConnectExpired => 'Quick Connect изтече. Опитайте отново.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Плътност на библиотеката';
|
||||
@override String get compact => 'Компактна';
|
||||
@override String get comfortable => 'Удобна';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Режим на изглед';
|
||||
@override String get gridView => 'Мрежа';
|
||||
@override String get listView => 'Списък';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Релей сървър за гледане заедно';
|
||||
@override String get watchTogetherRelayDescription => 'Задай собствен релей сървър. Всички трябва да използват същия сървър.';
|
||||
@override String get watchTogetherRelayHint => 'https://my-relay.example.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Докладване на сривове';
|
||||
@override String get crashReportingDescription => 'Изпращай доклади за сривове, за да помогнеш за подобряване на приложението';
|
||||
@override String get debugLogging => 'Логове за отстраняване на грешки';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Експортирай настройките';
|
||||
@override String get exportSettingsDescription => 'Запази предпочитанията си във файл';
|
||||
@override String get exportSettingsSuccess => 'Настройките са експортирани';
|
||||
@override String get exportSettingsFailed => 'Настройките не можаха да бъдат експортирани';
|
||||
@override String get importSettings => 'Импортирай настройки';
|
||||
@override String get importSettingsDescription => 'Възстанови предпочитания от файл';
|
||||
@override String get importSettingsConfirm => 'Това ще замени текущите ви настройки. Продължавате ли?';
|
||||
@override String get importSettingsSuccess => 'Настройките са импортирани';
|
||||
@override String get importSettingsFailed => 'Настройките не можаха да бъдат импортирани';
|
||||
@override String get importSettingsInvalidFile => 'Този файл не е валиден експорт на настройки от Plezy';
|
||||
@override String get importSettingsNoUser => 'Влезте, преди да импортирате настройки';
|
||||
@override String get shortcutsReset => 'Клавишните комбинации са нулирани до подразбиране';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Продължителността трябва да е между ${min} и ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Клавишната комбинация вече е назначена за ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Клавишната комбинация е обновена за ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Автоматично прескачане';
|
||||
@override String get autoSkipIntro => 'Автоматично прескачане на интро';
|
||||
@override String get autoSkipIntroDescription => 'Автоматично прескачай интро маркери след няколко секунди';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Местоположението за изтегляния е променено';
|
||||
@override String get downloadLocationReset => 'Местоположението за изтегляния е върнато по подразбиране';
|
||||
@override String get downloadLocationInvalid => 'Избраната папка не е записваема';
|
||||
@override String get downloadLocationSelectError => 'Неуспешен избор на папка';
|
||||
@override String get downloadLocationPickerUnavailable => 'Изборът на папка не е наличен на това устройство';
|
||||
@override String get downloadOnWifiOnly => 'Изтегляне само през WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Предотвратявай изтегляния през мобилни данни';
|
||||
@override String get autoRemoveWatchedDownloads => 'Автоматично премахвай изгледаните изтегляния';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityBg extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Шестнадесетичен цвят';
|
||||
@override String get expandText => 'Разгъни текста';
|
||||
@override String get collapseText => 'Свий текста';
|
||||
@override String get alphabetNavigation => 'Навигация по азбуката';
|
||||
@override String get alphabetScrollHint => 'Плъзнете нагоре или надолу, за да преминете по буква';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Ред ${row} от ${rowCount}, колона ${column} от ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Ред ${row} от ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsBg extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Маркирай като негледано';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksBg extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksBg._(TranslationsBg root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsBg _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Аудио пътечка ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsBg extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsBg._(TranslationsBg root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesBg extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.';
|
||||
@override String get liveStreamInterrupted => 'Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.';
|
||||
@override String get fileInfoNotAvailable => 'Информацията за файла не е налична';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}';
|
||||
@override String get errorLoadingSeries => 'Грешка при зареждане на сериала';
|
||||
@override String get musicNotSupported => 'Възпроизвеждането на музика все още не се поддържа';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesBg extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.';
|
||||
@override String get borrowEmpty => 'Все още няма какво да се използва.';
|
||||
@override String get borrowEmptySubtitle => 'Първо свържете Plex или Jellyfin към друг профил.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'От ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Връзката е използвана.';
|
||||
@override String get borrowFailed => 'Неуспешно използване на връзка.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvBg extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Любими';
|
||||
@override String get reorderFavorites => 'Пренареди любимите';
|
||||
@override String get favoritesLoadFailed => 'Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Присъедини се към текуща сесия';
|
||||
@override String watchFromStart({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)';
|
||||
@override String get watchLive => 'Гледай на живо';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsBg {
|
||||
'auth.quickConnectWaiting' => 'Изчакване на одобрение…',
|
||||
'auth.quickConnectCancel' => 'Отказ',
|
||||
'auth.quickConnectExpired' => 'Quick Connect изтече. Опитайте отново.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Отказ',
|
||||
'common.save' => 'Запази',
|
||||
'common.close' => 'Затвори',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsBg {
|
||||
'settings.libraryDensity' => 'Плътност на библиотеката',
|
||||
'settings.compact' => 'Компактна',
|
||||
'settings.comfortable' => 'Удобна',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Режим на изглед',
|
||||
'settings.gridView' => 'Мрежа',
|
||||
'settings.listView' => 'Списък',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsBg {
|
||||
'settings.watchTogetherRelay' => 'Релей сървър за гледане заедно',
|
||||
'settings.watchTogetherRelayDescription' => 'Задай собствен релей сървър. Всички трябва да използват същия сървър.',
|
||||
'settings.watchTogetherRelayHint' => 'https://my-relay.example.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Докладване на сривове',
|
||||
'settings.crashReportingDescription' => 'Изпращай доклади за сривове, за да помогнеш за подобряване на приложението',
|
||||
'settings.debugLogging' => 'Логове за отстраняване на грешки',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsBg {
|
||||
'settings.exportSettings' => 'Експортирай настройките',
|
||||
'settings.exportSettingsDescription' => 'Запази предпочитанията си във файл',
|
||||
'settings.exportSettingsSuccess' => 'Настройките са експортирани',
|
||||
'settings.exportSettingsFailed' => 'Настройките не можаха да бъдат експортирани',
|
||||
'settings.importSettings' => 'Импортирай настройки',
|
||||
'settings.importSettingsDescription' => 'Възстанови предпочитания от файл',
|
||||
'settings.importSettingsConfirm' => 'Това ще замени текущите ви настройки. Продължавате ли?',
|
||||
'settings.importSettingsSuccess' => 'Настройките са импортирани',
|
||||
'settings.importSettingsFailed' => 'Настройките не можаха да бъдат импортирани',
|
||||
'settings.importSettingsInvalidFile' => 'Този файл не е валиден експорт на настройки от Plezy',
|
||||
'settings.importSettingsNoUser' => 'Влезте, преди да импортирате настройки',
|
||||
'settings.shortcutsReset' => 'Клавишните комбинации са нулирани до подразбиране',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsBg {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Продължителността трябва да е между ${min} и ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Клавишната комбинация вече е назначена за ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Клавишната комбинация е обновена за ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Автоматично прескачане',
|
||||
'settings.autoSkipIntro' => 'Автоматично прескачане на интро',
|
||||
'settings.autoSkipIntroDescription' => 'Автоматично прескачай интро маркери след няколко секунди',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsBg {
|
||||
'settings.downloadLocationChanged' => 'Местоположението за изтегляния е променено',
|
||||
'settings.downloadLocationReset' => 'Местоположението за изтегляния е върнато по подразбиране',
|
||||
'settings.downloadLocationInvalid' => 'Избраната папка не е записваема',
|
||||
'settings.downloadLocationSelectError' => 'Неуспешен избор на папка',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Изборът на папка не е наличен на това устройство',
|
||||
'settings.downloadOnWifiOnly' => 'Изтегляне само през WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Предотвратявай изтегляния през мобилни данни',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Автоматично премахвай изгледаните изтегляния',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsBg {
|
||||
'accessibility.hexColor' => 'Шестнадесетичен цвят',
|
||||
'accessibility.expandText' => 'Разгъни текста',
|
||||
'accessibility.collapseText' => 'Свий текста',
|
||||
'accessibility.alphabetNavigation' => 'Навигация по азбуката',
|
||||
'accessibility.alphabetScrollHint' => 'Плъзнете нагоре или надолу, за да преминете по буква',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Ред ${row} от ${rowCount}, колона ${column} от ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Ред ${row} от ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Разбъркано възпроизвеждане',
|
||||
'tooltips.playTrailer' => 'Пусни трейлър',
|
||||
'tooltips.markAsWatched' => 'Маркирай като гледано',
|
||||
'tooltips.markAsUnwatched' => 'Маркирай като негледано',
|
||||
'audioTracks.track' => ({required Object n}) => 'Аудио пътечка ${n}',
|
||||
'videoControls.audioLabel' => 'Аудио',
|
||||
'videoControls.subtitlesLabel' => 'Субтитри',
|
||||
'videoControls.resetToZero' => 'Нулирай до 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsBg {
|
||||
'messages.markedAsUnwatched' => 'Маркирано като негледано',
|
||||
'messages.markedAsWatchedOffline' => 'Маркирано като гледано (ще се синхронизира, когато сте онлайн)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Маркирано като негледано (ще се синхронизира, когато сте онлайн)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Автоматично премахнато: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('bg'))(n, one: 'Автоматично премахнато ${n} гледано изтегляне', other: 'Автоматично премахнати ${n} гледани изтегляния', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Автоматично премахнато: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('bg'))(n, one: 'Автоматично премахнато ${n} гледано изтегляне', other: 'Автоматично премахнати ${n} гледани изтегляния', ),
|
||||
'messages.removedFromContinueWatching' => 'Премахнато от продължаване на гледането',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Грешка: ${error}',
|
||||
'messages.streamInterrupted' => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.',
|
||||
'messages.liveStreamInterrupted' => 'Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.',
|
||||
'messages.fileInfoNotAvailable' => 'Информацията за файла не е налична',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Грешка при зареждане на сериала',
|
||||
'messages.musicNotSupported' => 'Възпроизвеждането на музика все още не се поддържа',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsBg {
|
||||
'profiles.borrowExplain' => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.',
|
||||
'profiles.borrowEmpty' => 'Все още няма какво да се използва.',
|
||||
'profiles.borrowEmptySubtitle' => 'Първо свържете Plex или Jellyfin към друг профил.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'От ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Връзката е използвана.',
|
||||
'profiles.borrowFailed' => 'Неуспешно използване на връзка.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsBg {
|
||||
'liveTv.favorites' => 'Любими',
|
||||
'liveTv.reorderFavorites' => 'Пренареди любимите',
|
||||
'liveTv.favoritesLoadFailed' => 'Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Присъедини се към текуща сесия',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)',
|
||||
'liveTv.watchLive' => 'Гледай на живо',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsBg {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} напусна',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} постави на пауза',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} продължи',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} превъртя',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} буферира',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} е с по-стара версия на приложението — синхронизирането не е налично',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Продължаване без ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Изчакване другите да заредят...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Изчакване на ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsDa extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetDa rateSheet = _TranslationsRateSheetDa._(_root);
|
||||
@override late final _TranslationsAccessibilityDa accessibility = _TranslationsAccessibilityDa._(_root);
|
||||
@override late final _TranslationsTooltipsDa tooltips = _TranslationsTooltipsDa._(_root);
|
||||
@override late final _TranslationsAudioTracksDa audioTracks = _TranslationsAudioTracksDa._(_root);
|
||||
@override late final _TranslationsVideoControlsDa videoControls = _TranslationsVideoControlsDa._(_root);
|
||||
@override late final _TranslationsMessagesDa messages = _TranslationsMessagesDa._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingDa subtitlingStyling = _TranslationsSubtitlingStylingDa._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthDa extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Venter på godkendelse…';
|
||||
@override String get quickConnectCancel => 'Annullér';
|
||||
@override String get quickConnectExpired => 'Quick Connect er udløbet. Prøv igen.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Bibliotekstæthed';
|
||||
@override String get compact => 'Kompakt';
|
||||
@override String get comfortable => 'Komfortabel';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Visningstilstand';
|
||||
@override String get gridView => 'Gitter';
|
||||
@override String get listView => 'Liste';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Watch Together-relay';
|
||||
@override String get watchTogetherRelayDescription => 'Angiv en brugerdefineret relay. Alle skal bruge samme server.';
|
||||
@override String get watchTogetherRelayHint => 'https://min-relay.eksempel.dk';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Fejlrapportering';
|
||||
@override String get crashReportingDescription => 'Send fejlrapporter for at hjælpe med at forbedre appen';
|
||||
@override String get debugLogging => 'Fejlfindingslogning';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Eksportér indstillinger';
|
||||
@override String get exportSettingsDescription => 'Gem dine præferencer i en fil';
|
||||
@override String get exportSettingsSuccess => 'Indstillinger eksporteret';
|
||||
@override String get exportSettingsFailed => 'Kunne ikke eksportere indstillinger';
|
||||
@override String get importSettings => 'Importér indstillinger';
|
||||
@override String get importSettingsDescription => 'Gendan præferencer fra en fil';
|
||||
@override String get importSettingsConfirm => 'Dette vil erstatte dine nuværende indstillinger. Fortsæt?';
|
||||
@override String get importSettingsSuccess => 'Indstillinger importeret';
|
||||
@override String get importSettingsFailed => 'Kunne ikke importere indstillinger';
|
||||
@override String get importSettingsInvalidFile => 'Denne fil er ikke en gyldig Plezy-indstillingseksport';
|
||||
@override String get importSettingsNoUser => 'Log ind før import af indstillinger';
|
||||
@override String get shortcutsReset => 'Genveje nulstillet til standard';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Varighed skal være mellem ${min} og ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Genvej allerede tildelt til ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Genvej opdateret for ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Auto-spring';
|
||||
@override String get autoSkipIntro => 'Auto-spring intro';
|
||||
@override String get autoSkipIntroDescription => 'Spring automatisk intromarkører over efter få sekunder';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Downloadplacering ændret';
|
||||
@override String get downloadLocationReset => 'Downloadplacering nulstillet';
|
||||
@override String get downloadLocationInvalid => 'Valgt mappe er ikke skrivbar';
|
||||
@override String get downloadLocationSelectError => 'Kunne ikke vælge mappe';
|
||||
@override String get downloadLocationPickerUnavailable => 'Mappevalg er ikke tilgængeligt på denne enhed';
|
||||
@override String get downloadOnWifiOnly => 'Download kun på WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Forhindre downloads på mobildata';
|
||||
@override String get autoRemoveWatchedDownloads => 'Fjern sete downloads automatisk';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityDa extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Hexfarve';
|
||||
@override String get expandText => 'Udvid tekst';
|
||||
@override String get collapseText => 'Fold tekst sammen';
|
||||
@override String get alphabetNavigation => 'Alfabetnavigation';
|
||||
@override String get alphabetScrollHint => 'Stryg op eller ned for at flytte ét bogstav';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Række ${row} af ${rowCount}, kolonne ${column} af ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Række ${row} af ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsDa extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Markér som uset';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksDa extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksDa._(TranslationsDa root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsDa _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Lydspor ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsDa extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsDa._(TranslationsDa root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesDa extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.';
|
||||
@override String get liveStreamInterrupted => 'Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.';
|
||||
@override String get fileInfoNotAvailable => 'Filinfo ikke tilgængelig';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}';
|
||||
@override String get errorLoadingSeries => 'Fejl ved indlæsning af serie';
|
||||
@override String get musicNotSupported => 'Musikafspilning understøttes endnu ikke';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesDa extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Lån en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.';
|
||||
@override String get borrowEmpty => 'Intet at låne endnu.';
|
||||
@override String get borrowEmptySubtitle => 'Forbind Plex eller Jellyfin til en anden profil først.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Fra ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Forbindelse lånt.';
|
||||
@override String get borrowFailed => 'Kunne ikke låne forbindelse.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvDa extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoritter';
|
||||
@override String get reorderFavorites => 'Omarranger favoritter';
|
||||
@override String get favoritesLoadFailed => 'Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Deltag i igangværende session';
|
||||
@override String watchFromStart({required Object minutes}) => 'Se fra start (${minutes} min siden)';
|
||||
@override String get watchLive => 'Se live';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsDa {
|
||||
'auth.quickConnectWaiting' => 'Venter på godkendelse…',
|
||||
'auth.quickConnectCancel' => 'Annullér',
|
||||
'auth.quickConnectExpired' => 'Quick Connect er udløbet. Prøv igen.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Annuller',
|
||||
'common.save' => 'Gem',
|
||||
'common.close' => 'Luk',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsDa {
|
||||
'settings.libraryDensity' => 'Bibliotekstæthed',
|
||||
'settings.compact' => 'Kompakt',
|
||||
'settings.comfortable' => 'Komfortabel',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Visningstilstand',
|
||||
'settings.gridView' => 'Gitter',
|
||||
'settings.listView' => 'Liste',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsDa {
|
||||
'settings.watchTogetherRelay' => 'Watch Together-relay',
|
||||
'settings.watchTogetherRelayDescription' => 'Angiv en brugerdefineret relay. Alle skal bruge samme server.',
|
||||
'settings.watchTogetherRelayHint' => 'https://min-relay.eksempel.dk',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Fejlrapportering',
|
||||
'settings.crashReportingDescription' => 'Send fejlrapporter for at hjælpe med at forbedre appen',
|
||||
'settings.debugLogging' => 'Fejlfindingslogning',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsDa {
|
||||
'settings.exportSettings' => 'Eksportér indstillinger',
|
||||
'settings.exportSettingsDescription' => 'Gem dine præferencer i en fil',
|
||||
'settings.exportSettingsSuccess' => 'Indstillinger eksporteret',
|
||||
'settings.exportSettingsFailed' => 'Kunne ikke eksportere indstillinger',
|
||||
'settings.importSettings' => 'Importér indstillinger',
|
||||
'settings.importSettingsDescription' => 'Gendan præferencer fra en fil',
|
||||
'settings.importSettingsConfirm' => 'Dette vil erstatte dine nuværende indstillinger. Fortsæt?',
|
||||
'settings.importSettingsSuccess' => 'Indstillinger importeret',
|
||||
'settings.importSettingsFailed' => 'Kunne ikke importere indstillinger',
|
||||
'settings.importSettingsInvalidFile' => 'Denne fil er ikke en gyldig Plezy-indstillingseksport',
|
||||
'settings.importSettingsNoUser' => 'Log ind før import af indstillinger',
|
||||
'settings.shortcutsReset' => 'Genveje nulstillet til standard',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsDa {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Varighed skal være mellem ${min} og ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Genvej allerede tildelt til ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Genvej opdateret for ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Auto-spring',
|
||||
'settings.autoSkipIntro' => 'Auto-spring intro',
|
||||
'settings.autoSkipIntroDescription' => 'Spring automatisk intromarkører over efter få sekunder',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsDa {
|
||||
'settings.downloadLocationChanged' => 'Downloadplacering ændret',
|
||||
'settings.downloadLocationReset' => 'Downloadplacering nulstillet',
|
||||
'settings.downloadLocationInvalid' => 'Valgt mappe er ikke skrivbar',
|
||||
'settings.downloadLocationSelectError' => 'Kunne ikke vælge mappe',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Mappevalg er ikke tilgængeligt på denne enhed',
|
||||
'settings.downloadOnWifiOnly' => 'Download kun på WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Forhindre downloads på mobildata',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Fjern sete downloads automatisk',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsDa {
|
||||
'accessibility.hexColor' => 'Hexfarve',
|
||||
'accessibility.expandText' => 'Udvid tekst',
|
||||
'accessibility.collapseText' => 'Fold tekst sammen',
|
||||
'accessibility.alphabetNavigation' => 'Alfabetnavigation',
|
||||
'accessibility.alphabetScrollHint' => 'Stryg op eller ned for at flytte ét bogstav',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Række ${row} af ${rowCount}, kolonne ${column} af ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Række ${row} af ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Afspil tilfældigt',
|
||||
'tooltips.playTrailer' => 'Afspil trailer',
|
||||
'tooltips.markAsWatched' => 'Markér som set',
|
||||
'tooltips.markAsUnwatched' => 'Markér som uset',
|
||||
'audioTracks.track' => ({required Object n}) => 'Lydspor ${n}',
|
||||
'videoControls.audioLabel' => 'Lyd',
|
||||
'videoControls.subtitlesLabel' => 'Undertekster',
|
||||
'videoControls.resetToZero' => 'Nulstil til 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsDa {
|
||||
'messages.markedAsUnwatched' => 'Markeret som uset',
|
||||
'messages.markedAsWatchedOffline' => 'Markeret som set (synkroniseres online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Markeret som uset (synkroniseres online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisk fjernet: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('da'))(n, one: 'Fjernede automatisk ${n} set download', other: 'Fjernede automatisk ${n} sete downloads', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisk fjernet: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('da'))(n, one: 'Fjernede automatisk ${n} set download', other: 'Fjernede automatisk ${n} sete downloads', ),
|
||||
'messages.removedFromContinueWatching' => 'Fjernet fra Fortsæt med at se',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Fejl: ${error}',
|
||||
'messages.streamInterrupted' => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.',
|
||||
'messages.liveStreamInterrupted' => 'Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.',
|
||||
'messages.fileInfoNotAvailable' => 'Filinfo ikke tilgængelig',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Fejl ved indlæsning af serie',
|
||||
'messages.musicNotSupported' => 'Musikafspilning understøttes endnu ikke',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsDa {
|
||||
'profiles.borrowExplain' => 'Lån en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.',
|
||||
'profiles.borrowEmpty' => 'Intet at låne endnu.',
|
||||
'profiles.borrowEmptySubtitle' => 'Forbind Plex eller Jellyfin til en anden profil først.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Fra ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Forbindelse lånt.',
|
||||
'profiles.borrowFailed' => 'Kunne ikke låne forbindelse.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsDa {
|
||||
'liveTv.favorites' => 'Favoritter',
|
||||
'liveTv.reorderFavorites' => 'Omarranger favoritter',
|
||||
'liveTv.favoritesLoadFailed' => 'Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Deltag i igangværende session',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra start (${minutes} min siden)',
|
||||
'liveTv.watchLive' => 'Se live',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsDa {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} forlod',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} satte på pause',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} genoptog',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} spoled',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} bufferer',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} bruger en ældre appversion — synkronisering er ikke tilgængelig',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Fortsætter uden ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Venter på at andre indlæser...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Venter på ${name}...',
|
||||
|
||||
+46
-26
@@ -52,6 +52,7 @@ class TranslationsDe extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetDe rateSheet = _TranslationsRateSheetDe._(_root);
|
||||
@override late final _TranslationsAccessibilityDe accessibility = _TranslationsAccessibilityDe._(_root);
|
||||
@override late final _TranslationsTooltipsDe tooltips = _TranslationsTooltipsDe._(_root);
|
||||
@override late final _TranslationsAudioTracksDe audioTracks = _TranslationsAudioTracksDe._(_root);
|
||||
@override late final _TranslationsVideoControlsDe videoControls = _TranslationsVideoControlsDe._(_root);
|
||||
@override late final _TranslationsMessagesDe messages = _TranslationsMessagesDe._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingDe subtitlingStyling = _TranslationsSubtitlingStylingDe._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthDe extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Warte auf Bestätigung…';
|
||||
@override String get quickConnectCancel => 'Abbrechen';
|
||||
@override String get quickConnectExpired => 'Quick Connect ist abgelaufen. Versuche es erneut.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -311,7 +312,7 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Gemeinsam Schauen Relay';
|
||||
@override String get watchTogetherRelayDescription => 'Eigenes Relay festlegen. Alle müssen denselben Server verwenden.';
|
||||
@override String get watchTogetherRelayHint => 'https://mein-relay.beispiel.de';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Absturzberichte';
|
||||
@override String get crashReportingDescription => 'Absturzberichte senden, um die App zu verbessern';
|
||||
@override String get debugLogging => 'Debug-Protokollierung';
|
||||
@@ -328,12 +329,10 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Einstellungen exportieren';
|
||||
@override String get exportSettingsDescription => 'Speichere deine Einstellungen in einer Datei';
|
||||
@override String get exportSettingsSuccess => 'Einstellungen exportiert';
|
||||
@override String get exportSettingsFailed => 'Einstellungen konnten nicht exportiert werden';
|
||||
@override String get importSettings => 'Einstellungen importieren';
|
||||
@override String get importSettingsDescription => 'Einstellungen aus einer Datei wiederherstellen';
|
||||
@override String get importSettingsConfirm => 'Dies ersetzt deine aktuellen Einstellungen. Fortfahren?';
|
||||
@override String get importSettingsSuccess => 'Einstellungen importiert';
|
||||
@override String get importSettingsFailed => 'Einstellungen konnten nicht importiert werden';
|
||||
@override String get importSettingsInvalidFile => 'Diese Datei ist kein gültiger Plezy-Einstellungsexport';
|
||||
@override String get importSettingsNoUser => 'Vor dem Import bitte anmelden';
|
||||
@override String get shortcutsReset => 'Tastenkürzel auf Standard zurückgesetzt';
|
||||
@@ -348,6 +347,7 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Dauer muss zwischen ${min} und ${max} ${unit} liegen';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Tastenkürzel bereits zugewiesen an ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Tastenkürzel aktualisiert für ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Automatisches Überspringen';
|
||||
@override String get autoSkipIntro => 'Intro automatisch überspringen';
|
||||
@override String get autoSkipIntroDescription => 'Intro-Marker nach wenigen Sekunden automatisch überspringen';
|
||||
@@ -373,7 +373,7 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Download-Speicherort geändert';
|
||||
@override String get downloadLocationReset => 'Download-Speicherort auf Standard zurückgesetzt';
|
||||
@override String get downloadLocationInvalid => 'Ausgewählter Ordner ist nicht beschreibbar';
|
||||
@override String get downloadLocationSelectError => 'Ordnerauswahl fehlgeschlagen';
|
||||
@override String get downloadLocationPickerUnavailable => 'Die Ordnerauswahl ist auf diesem Gerät nicht verfügbar';
|
||||
@override String get downloadOnWifiOnly => 'Nur über WLAN herunterladen';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Downloads über mobile Daten verhindern';
|
||||
@override String get autoRemoveWatchedDownloads => 'Gesehene Downloads automatisch entfernen';
|
||||
@@ -591,6 +591,10 @@ class _TranslationsAccessibilityDe extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Hex-Farbe';
|
||||
@override String get expandText => 'Text ausklappen';
|
||||
@override String get collapseText => 'Text einklappen';
|
||||
@override String get alphabetNavigation => 'Alphabetische Navigation';
|
||||
@override String get alphabetScrollHint => 'Nach oben oder unten wischen, um einen Buchstaben weiterzugehen';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Zeile ${row} von ${rowCount}, Spalte ${column} von ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Zeile ${row} von ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +610,16 @@ class _TranslationsTooltipsDe extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Als ungesehen markieren';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksDe extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksDe._(TranslationsDe root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsDe _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Audiospur ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsDe extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsDe._(TranslationsDe root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +726,11 @@ class _TranslationsMessagesDe extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.';
|
||||
@override String get liveStreamInterrupted => 'Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.';
|
||||
@override String get fileInfoNotAvailable => 'Dateiinfo nicht verfügbar';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}';
|
||||
@override String get errorLoadingSeries => 'Fehler beim Laden der Serie';
|
||||
@override String get musicNotSupported => 'Musikwiedergabe wird noch nicht unterstützt';
|
||||
@@ -867,6 +881,7 @@ class _TranslationsProfilesDe extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Verbindung eines anderen Profils leihen. PIN-geschützte Profile erfordern eine PIN.';
|
||||
@override String get borrowEmpty => 'Noch nichts zum Ausleihen.';
|
||||
@override String get borrowEmptySubtitle => 'Verbinde zuerst Plex oder Jellyfin mit einem anderen Profil.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Von ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Verbindung ausgeliehen.';
|
||||
@override String get borrowFailed => 'Verbindung konnte nicht ausgeliehen werden.';
|
||||
@@ -1150,7 +1165,7 @@ class _TranslationsLiveTvDe extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoriten';
|
||||
@override String get reorderFavorites => 'Favoriten sortieren';
|
||||
@override String get favoritesLoadFailed => 'Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Laufender Sitzung beitreten';
|
||||
@override String watchFromStart({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)';
|
||||
@override String get watchLive => 'Live ansehen';
|
||||
@@ -2149,7 +2164,7 @@ extension on TranslationsDe {
|
||||
'auth.quickConnectWaiting' => 'Warte auf Bestätigung…',
|
||||
'auth.quickConnectCancel' => 'Abbrechen',
|
||||
'auth.quickConnectExpired' => 'Quick Connect ist abgelaufen. Versuche es erneut.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Abbrechen',
|
||||
'common.save' => 'Speichern',
|
||||
'common.close' => 'Schließen',
|
||||
@@ -2304,7 +2319,7 @@ extension on TranslationsDe {
|
||||
'settings.watchTogetherRelay' => 'Gemeinsam Schauen Relay',
|
||||
'settings.watchTogetherRelayDescription' => 'Eigenes Relay festlegen. Alle müssen denselben Server verwenden.',
|
||||
'settings.watchTogetherRelayHint' => 'https://mein-relay.beispiel.de',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Absturzberichte',
|
||||
'settings.crashReportingDescription' => 'Absturzberichte senden, um die App zu verbessern',
|
||||
'settings.debugLogging' => 'Debug-Protokollierung',
|
||||
@@ -2321,12 +2336,10 @@ extension on TranslationsDe {
|
||||
'settings.exportSettings' => 'Einstellungen exportieren',
|
||||
'settings.exportSettingsDescription' => 'Speichere deine Einstellungen in einer Datei',
|
||||
'settings.exportSettingsSuccess' => 'Einstellungen exportiert',
|
||||
'settings.exportSettingsFailed' => 'Einstellungen konnten nicht exportiert werden',
|
||||
'settings.importSettings' => 'Einstellungen importieren',
|
||||
'settings.importSettingsDescription' => 'Einstellungen aus einer Datei wiederherstellen',
|
||||
'settings.importSettingsConfirm' => 'Dies ersetzt deine aktuellen Einstellungen. Fortfahren?',
|
||||
'settings.importSettingsSuccess' => 'Einstellungen importiert',
|
||||
'settings.importSettingsFailed' => 'Einstellungen konnten nicht importiert werden',
|
||||
'settings.importSettingsInvalidFile' => 'Diese Datei ist kein gültiger Plezy-Einstellungsexport',
|
||||
'settings.importSettingsNoUser' => 'Vor dem Import bitte anmelden',
|
||||
'settings.shortcutsReset' => 'Tastenkürzel auf Standard zurückgesetzt',
|
||||
@@ -2341,6 +2354,7 @@ extension on TranslationsDe {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Dauer muss zwischen ${min} und ${max} ${unit} liegen',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Tastenkürzel bereits zugewiesen an ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Tastenkürzel aktualisiert für ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Automatisches Überspringen',
|
||||
'settings.autoSkipIntro' => 'Intro automatisch überspringen',
|
||||
'settings.autoSkipIntroDescription' => 'Intro-Marker nach wenigen Sekunden automatisch überspringen',
|
||||
@@ -2366,7 +2380,7 @@ extension on TranslationsDe {
|
||||
'settings.downloadLocationChanged' => 'Download-Speicherort geändert',
|
||||
'settings.downloadLocationReset' => 'Download-Speicherort auf Standard zurückgesetzt',
|
||||
'settings.downloadLocationInvalid' => 'Ausgewählter Ordner ist nicht beschreibbar',
|
||||
'settings.downloadLocationSelectError' => 'Ordnerauswahl fehlgeschlagen',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Die Ordnerauswahl ist auf diesem Gerät nicht verfügbar',
|
||||
'settings.downloadOnWifiOnly' => 'Nur über WLAN herunterladen',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Downloads über mobile Daten verhindern',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Gesehene Downloads automatisch entfernen',
|
||||
@@ -2554,10 +2568,15 @@ extension on TranslationsDe {
|
||||
'accessibility.hexColor' => 'Hex-Farbe',
|
||||
'accessibility.expandText' => 'Text ausklappen',
|
||||
'accessibility.collapseText' => 'Text einklappen',
|
||||
'accessibility.alphabetNavigation' => 'Alphabetische Navigation',
|
||||
'accessibility.alphabetScrollHint' => 'Nach oben oder unten wischen, um einen Buchstaben weiterzugehen',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Zeile ${row} von ${rowCount}, Spalte ${column} von ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Zeile ${row} von ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Zufallswiedergabe',
|
||||
'tooltips.playTrailer' => 'Trailer abspielen',
|
||||
'tooltips.markAsWatched' => 'Als gesehen markieren',
|
||||
'tooltips.markAsUnwatched' => 'Als ungesehen markieren',
|
||||
'audioTracks.track' => ({required Object n}) => 'Audiospur ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Untertitel',
|
||||
'videoControls.resetToZero' => 'Auf 0 ms zurücksetzen',
|
||||
@@ -2642,22 +2661,22 @@ extension on TranslationsDe {
|
||||
'videoControls.searchLanguages' => 'Sprachen suchen...',
|
||||
'messages.markedAsWatched' => 'Als gesehen markiert',
|
||||
'messages.markedAsUnwatched' => 'Als ungesehen markiert',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.markedAsWatchedOffline' => 'Als gesehen markiert (wird synchronisiert, wenn online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Als ungesehen markiert (wird synchronisiert, wenn online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisch entfernt: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, one: 'Automatisch entfernt: ${n} angesehener Download', other: 'Automatisch entfernt: ${n} angesehene Downloads', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.removedFromContinueWatching' => 'Aus ‚Weiterschauen\' entfernt',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Fehler: ${error}',
|
||||
'messages.streamInterrupted' => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.',
|
||||
'messages.liveStreamInterrupted' => 'Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.',
|
||||
'messages.fileInfoNotAvailable' => 'Dateiinfo nicht verfügbar',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Fehler beim Laden der Serie',
|
||||
'messages.musicNotSupported' => 'Musikwiedergabe wird noch nicht unterstützt',
|
||||
@@ -2772,6 +2791,7 @@ extension on TranslationsDe {
|
||||
'profiles.borrowExplain' => 'Verbindung eines anderen Profils leihen. PIN-geschützte Profile erfordern eine PIN.',
|
||||
'profiles.borrowEmpty' => 'Noch nichts zum Ausleihen.',
|
||||
'profiles.borrowEmptySubtitle' => 'Verbinde zuerst Plex oder Jellyfin mit einem anderen Profil.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Von ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Verbindung ausgeliehen.',
|
||||
'profiles.borrowFailed' => 'Verbindung konnte nicht ausgeliehen werden.',
|
||||
@@ -2997,7 +3017,7 @@ extension on TranslationsDe {
|
||||
'liveTv.favorites' => 'Favoriten',
|
||||
'liveTv.reorderFavorites' => 'Favoriten sortieren',
|
||||
'liveTv.favoritesLoadFailed' => 'Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Laufender Sitzung beitreten',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)',
|
||||
'liveTv.watchLive' => 'Live ansehen',
|
||||
@@ -3155,13 +3175,13 @@ extension on TranslationsDe {
|
||||
'watchTogether.failedToOpenCurrentPlayback' => 'Aktuelle Wiedergabe konnte nicht geöffnet werden',
|
||||
'watchTogether.participantJoined' => ({required Object name}) => '${name} ist beigetreten',
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} hat die Sitzung verlassen',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} hat pausiert',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} hat fortgesetzt',
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} hat gespult',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} puffert',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} verwendet eine ältere Appversion — Synchronisierung nicht verfügbar',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Fortfahren ohne ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Warte auf andere zum Laden...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Warten auf ${name}...',
|
||||
|
||||
+57
-15
@@ -53,6 +53,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||
late final TranslationsRateSheetEn rateSheet = TranslationsRateSheetEn.internal(_root);
|
||||
late final TranslationsAccessibilityEn accessibility = TranslationsAccessibilityEn.internal(_root);
|
||||
late final TranslationsTooltipsEn tooltips = TranslationsTooltipsEn.internal(_root);
|
||||
late final TranslationsAudioTracksEn audioTracks = TranslationsAudioTracksEn.internal(_root);
|
||||
late final TranslationsVideoControlsEn videoControls = TranslationsVideoControlsEn.internal(_root);
|
||||
late final TranslationsMessagesEn messages = TranslationsMessagesEn.internal(_root);
|
||||
late final TranslationsSubtitlingStylingEn subtitlingStyling = TranslationsSubtitlingStylingEn.internal(_root);
|
||||
@@ -662,6 +663,9 @@ class TranslationsSettingsEn {
|
||||
/// en: 'Send crash reports to help improve the app'
|
||||
String get crashReportingDescription => 'Send crash reports to help improve the app';
|
||||
|
||||
/// en: 'After disabling, reports already accepted, queued, or being sent may finish. Enabling again takes effect when Plezy restarts.'
|
||||
String get crashReportingRestartRequired => 'After disabling, reports already accepted, queued, or being sent may finish. Enabling again takes effect when Plezy restarts.';
|
||||
|
||||
/// en: 'Debug Logging'
|
||||
String get debugLogging => 'Debug Logging';
|
||||
|
||||
@@ -704,9 +708,6 @@ class TranslationsSettingsEn {
|
||||
/// en: 'Settings exported'
|
||||
String get exportSettingsSuccess => 'Settings exported';
|
||||
|
||||
/// en: 'Could not export settings'
|
||||
String get exportSettingsFailed => 'Could not export settings';
|
||||
|
||||
/// en: 'Import Settings'
|
||||
String get importSettings => 'Import Settings';
|
||||
|
||||
@@ -719,9 +720,6 @@ class TranslationsSettingsEn {
|
||||
/// en: 'Settings imported'
|
||||
String get importSettingsSuccess => 'Settings imported';
|
||||
|
||||
/// en: 'Could not import settings'
|
||||
String get importSettingsFailed => 'Could not import settings';
|
||||
|
||||
/// en: 'This file isn't a valid Plezy settings export'
|
||||
String get importSettingsInvalidFile => 'This file isn\'t a valid Plezy settings export';
|
||||
|
||||
@@ -764,6 +762,9 @@ class TranslationsSettingsEn {
|
||||
/// en: 'Shortcut updated for ${action}'
|
||||
String shortcutUpdated({required Object action}) => 'Shortcut updated for ${action}';
|
||||
|
||||
/// en: 'Could not save changes. Try again.'
|
||||
String get saveFailed => 'Could not save changes. Try again.';
|
||||
|
||||
/// en: 'Auto Skip'
|
||||
String get autoSkip => 'Auto Skip';
|
||||
|
||||
@@ -839,8 +840,8 @@ class TranslationsSettingsEn {
|
||||
/// en: 'Selected folder is not writable'
|
||||
String get downloadLocationInvalid => 'Selected folder is not writable';
|
||||
|
||||
/// en: 'Failed to select folder'
|
||||
String get downloadLocationSelectError => 'Failed to select folder';
|
||||
/// en: 'Folder selection is not available on this device'
|
||||
String get downloadLocationPickerUnavailable => 'Folder selection is not available on this device';
|
||||
|
||||
/// en: 'Download on WiFi only'
|
||||
String get downloadOnWifiOnly => 'Download on WiFi only';
|
||||
@@ -1383,6 +1384,18 @@ class TranslationsAccessibilityEn {
|
||||
|
||||
/// en: 'Collapse text'
|
||||
String get collapseText => 'Collapse text';
|
||||
|
||||
/// en: 'Alphabet navigation'
|
||||
String get alphabetNavigation => 'Alphabet navigation';
|
||||
|
||||
/// en: 'Swipe up or down to move by letter'
|
||||
String get alphabetScrollHint => 'Swipe up or down to move by letter';
|
||||
|
||||
/// en: 'Row ${row} of ${rowCount}, column ${column} of ${columnCount}'
|
||||
String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Row ${row} of ${rowCount}, column ${column} of ${columnCount}';
|
||||
|
||||
/// en: 'Row ${row} of ${rowCount}'
|
||||
String rowPosition({required Object row, required Object rowCount}) => 'Row ${row} of ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -1406,6 +1419,18 @@ class TranslationsTooltipsEn {
|
||||
String get markAsUnwatched => 'Mark as unwatched';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class TranslationsAudioTracksEn {
|
||||
TranslationsAudioTracksEn.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Audio Track ${n}'
|
||||
String track({required Object n}) => 'Audio Track ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class TranslationsVideoControlsEn {
|
||||
TranslationsVideoControlsEn.internal(this._root);
|
||||
@@ -1492,6 +1517,12 @@ class TranslationsVideoControlsEn {
|
||||
/// en: 'Pause'
|
||||
String get pauseButton => 'Pause';
|
||||
|
||||
/// en: 'Show playback controls'
|
||||
String get showPlaybackControls => 'Show playback controls';
|
||||
|
||||
/// en: 'Hide playback controls'
|
||||
String get hidePlaybackControls => 'Hide playback controls';
|
||||
|
||||
/// en: 'Seek backward ${seconds} seconds'
|
||||
String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds';
|
||||
|
||||
@@ -2079,6 +2110,9 @@ class TranslationsProfilesEn {
|
||||
/// en: 'Connect Plex or Jellyfin to another profile first.'
|
||||
String get borrowEmptySubtitle => 'Connect Plex or Jellyfin to another profile first.';
|
||||
|
||||
/// en: 'Available connections could not be loaded. Try again.'
|
||||
String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
|
||||
/// en: 'From ${displayName}'
|
||||
String borrowFromProfile({required Object displayName}) => 'From ${displayName}';
|
||||
|
||||
@@ -5186,6 +5220,7 @@ extension on Translations {
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Crash Reporting',
|
||||
'settings.crashReportingDescription' => 'Send crash reports to help improve the app',
|
||||
'settings.crashReportingRestartRequired' => 'After disabling, reports already accepted, queued, or being sent may finish. Enabling again takes effect when Plezy restarts.',
|
||||
'settings.debugLogging' => 'Debug Logging',
|
||||
'settings.debugLoggingDescription' => 'Enable detailed logging for troubleshooting',
|
||||
'settings.viewLogs' => 'View Logs',
|
||||
@@ -5200,12 +5235,10 @@ extension on Translations {
|
||||
'settings.exportSettings' => 'Export Settings',
|
||||
'settings.exportSettingsDescription' => 'Save your preferences to a file',
|
||||
'settings.exportSettingsSuccess' => 'Settings exported',
|
||||
'settings.exportSettingsFailed' => 'Could not export settings',
|
||||
'settings.importSettings' => 'Import Settings',
|
||||
'settings.importSettingsDescription' => 'Restore preferences from a file',
|
||||
'settings.importSettingsConfirm' => 'This will replace your current settings. Continue?',
|
||||
'settings.importSettingsSuccess' => 'Settings imported',
|
||||
'settings.importSettingsFailed' => 'Could not import settings',
|
||||
'settings.importSettingsInvalidFile' => 'This file isn\'t a valid Plezy settings export',
|
||||
'settings.importSettingsNoUser' => 'Sign in before importing settings',
|
||||
'settings.shortcutsReset' => 'Shortcuts reset to defaults',
|
||||
@@ -5220,6 +5253,7 @@ extension on Translations {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Duration must be between ${min} and ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Shortcut already assigned to ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Shortcut updated for ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Auto Skip',
|
||||
'settings.autoSkipIntro' => 'Auto Skip Intro',
|
||||
'settings.autoSkipIntroDescription' => 'Automatically skip intro markers after a few seconds',
|
||||
@@ -5245,7 +5279,7 @@ extension on Translations {
|
||||
'settings.downloadLocationChanged' => 'Download location changed',
|
||||
'settings.downloadLocationReset' => 'Download location reset to default',
|
||||
'settings.downloadLocationInvalid' => 'Selected folder is not writable',
|
||||
'settings.downloadLocationSelectError' => 'Failed to select folder',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Folder selection is not available on this device',
|
||||
'settings.downloadOnWifiOnly' => 'Download on WiFi only',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Prevent downloads when on cellular data',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Auto-remove watched downloads',
|
||||
@@ -5433,10 +5467,15 @@ extension on Translations {
|
||||
'accessibility.hexColor' => 'Hex color',
|
||||
'accessibility.expandText' => 'Expand text',
|
||||
'accessibility.collapseText' => 'Collapse text',
|
||||
'accessibility.alphabetNavigation' => 'Alphabet navigation',
|
||||
'accessibility.alphabetScrollHint' => 'Swipe up or down to move by letter',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Row ${row} of ${rowCount}, column ${column} of ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Row ${row} of ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Shuffle play',
|
||||
'tooltips.playTrailer' => 'Play trailer',
|
||||
'tooltips.markAsWatched' => 'Mark as watched',
|
||||
'tooltips.markAsUnwatched' => 'Mark as unwatched',
|
||||
'audioTracks.track' => ({required Object n}) => 'Audio Track ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Subtitles',
|
||||
'videoControls.resetToZero' => 'Reset to 0ms',
|
||||
@@ -5463,6 +5502,8 @@ extension on Translations {
|
||||
'videoControls.playNext' => 'Play Next',
|
||||
'videoControls.playButton' => 'Play',
|
||||
'videoControls.pauseButton' => 'Pause',
|
||||
'videoControls.showPlaybackControls' => 'Show playback controls',
|
||||
'videoControls.hidePlaybackControls' => 'Hide playback controls',
|
||||
'videoControls.seekBackwardButton' => ({required Object seconds}) => 'Seek backward ${seconds} seconds',
|
||||
'videoControls.seekForwardButton' => ({required Object seconds}) => 'Seek forward ${seconds} seconds',
|
||||
'videoControls.previousButton' => 'Previous episode',
|
||||
@@ -5518,6 +5559,8 @@ extension on Translations {
|
||||
'videoControls.subtitleDownloaded' => 'Subtitle downloaded',
|
||||
'videoControls.subtitleDownloadedNotApplied' => 'Subtitle downloaded, but it could not be selected',
|
||||
'videoControls.subtitleDownloadFailed' => 'Failed to download subtitle',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoControls.searchLanguages' => 'Search languages...',
|
||||
|
||||
'messages.markedAsWatched' => 'Marked as watched',
|
||||
@@ -5526,8 +5569,6 @@ extension on Translations {
|
||||
'messages.markedAsUnwatchedOffline' => 'Marked as unwatched (will sync when online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Auto-removed: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'Auto-removed ${n} watched download', other: 'Auto-removed ${n} watched downloads', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.removedFromContinueWatching' => 'Removed from Continue Watching',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Error: ${error}',
|
||||
'messages.streamInterrupted' => 'The stream was interrupted. Press play or seek to retry.',
|
||||
@@ -5652,6 +5693,7 @@ extension on Translations {
|
||||
'profiles.borrowExplain' => 'Borrow another profile\'s connection. PIN-protected profiles require a PIN.',
|
||||
'profiles.borrowEmpty' => 'Nothing to borrow yet.',
|
||||
'profiles.borrowEmptySubtitle' => 'Connect Plex or Jellyfin to another profile first.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'From ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Connection borrowed.',
|
||||
'profiles.borrowFailed' => 'Failed to borrow connection.',
|
||||
@@ -6032,6 +6074,8 @@ extension on Translations {
|
||||
'watchTogether.currentPlayback' => 'Current Playback',
|
||||
'watchTogether.joinCurrentPlayback' => 'Join Current Playback',
|
||||
'watchTogether.joinCurrentPlaybackDescription' => 'Jump back into what the host is currently watching',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.failedToOpenCurrentPlayback' => 'Failed to open current playback',
|
||||
|
||||
'watchTogether.participantJoined' => ({required Object name}) => '${name} joined',
|
||||
@@ -6041,8 +6085,6 @@ extension on Translations {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} seeked',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} is buffering',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} is on an older app version — sync unavailable',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Resuming without ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Waiting for others to load...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Waiting for ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetEs rateSheet = _TranslationsRateSheetEs._(_root);
|
||||
@override late final _TranslationsAccessibilityEs accessibility = _TranslationsAccessibilityEs._(_root);
|
||||
@override late final _TranslationsTooltipsEs tooltips = _TranslationsTooltipsEs._(_root);
|
||||
@override late final _TranslationsAudioTracksEs audioTracks = _TranslationsAudioTracksEs._(_root);
|
||||
@override late final _TranslationsVideoControlsEs videoControls = _TranslationsVideoControlsEs._(_root);
|
||||
@override late final _TranslationsMessagesEs messages = _TranslationsMessagesEs._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingEs subtitlingStyling = _TranslationsSubtitlingStylingEs._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthEs extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Esperando aprobación…';
|
||||
@override String get quickConnectCancel => 'Cancelar';
|
||||
@override String get quickConnectExpired => 'Quick Connect caducó. Inténtalo de nuevo.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Densidad de Biblioteca';
|
||||
@override String get compact => 'Compacto';
|
||||
@override String get comfortable => 'Cómodo';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Modo de Vista';
|
||||
@override String get gridView => 'Cuadrícula';
|
||||
@override String get listView => 'Lista';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relay de Ver Juntos';
|
||||
@override String get watchTogetherRelayDescription => 'Define un relay personalizado. Todos deben usar el mismo servidor.';
|
||||
@override String get watchTogetherRelayHint => 'https://mi-relay.ejemplo.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Informes de Errores';
|
||||
@override String get crashReportingDescription => 'Enviar informes de errores para mejorar la aplicación';
|
||||
@override String get debugLogging => 'Registro de Depuración';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Exportar Configuración';
|
||||
@override String get exportSettingsDescription => 'Guardar tus preferencias en un archivo';
|
||||
@override String get exportSettingsSuccess => 'Configuración exportada';
|
||||
@override String get exportSettingsFailed => 'No se pudo exportar la configuración';
|
||||
@override String get importSettings => 'Importar Configuración';
|
||||
@override String get importSettingsDescription => 'Restaurar preferencias desde un archivo';
|
||||
@override String get importSettingsConfirm => 'Esto reemplazará tu configuración actual. ¿Continuar?';
|
||||
@override String get importSettingsSuccess => 'Configuración importada';
|
||||
@override String get importSettingsFailed => 'No se pudo importar la configuración';
|
||||
@override String get importSettingsInvalidFile => 'Este archivo no es una exportación válida de Plezy';
|
||||
@override String get importSettingsNoUser => 'Inicia sesión antes de importar la configuración';
|
||||
@override String get shortcutsReset => 'Atajos restablecidos a los valores predeterminados';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'La duración debe estar entre ${min} y ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'El atajo ya está asignado a ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Atajo actualizado para ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Salto automático';
|
||||
@override String get autoSkipIntro => 'Saltar Intro automáticamente';
|
||||
@override String get autoSkipIntroDescription => 'Saltar automáticamente los marcadores de intro después de unos segundos';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Ubicación de descarga cambiada';
|
||||
@override String get downloadLocationReset => 'Ubicación de descarga restablecida al predeterminado';
|
||||
@override String get downloadLocationInvalid => 'La carpeta seleccionada no tiene permisos de escritura';
|
||||
@override String get downloadLocationSelectError => 'Error al seleccionar la carpeta';
|
||||
@override String get downloadLocationPickerUnavailable => 'La selección de carpetas no está disponible en este dispositivo';
|
||||
@override String get downloadOnWifiOnly => 'Descargar solo con WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Evitar descargas cuando se usan datos móviles';
|
||||
@override String get autoRemoveWatchedDownloads => 'Eliminar descargas vistas automáticamente';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityEs extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Color hexadecimal';
|
||||
@override String get expandText => 'Expandir texto';
|
||||
@override String get collapseText => 'Contraer texto';
|
||||
@override String get alphabetNavigation => 'Navegación alfabética';
|
||||
@override String get alphabetScrollHint => 'Desliza hacia arriba o abajo para avanzar por letra';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Fila ${row} de ${rowCount}, columna ${column} de ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Fila ${row} de ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsEs extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Marcar como no visto';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksEs extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksEs._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Pista de audio ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsEs extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsEs._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesEs extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.';
|
||||
@override String get liveStreamInterrupted => 'La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.';
|
||||
@override String get fileInfoNotAvailable => 'Información de archivo no disponible';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Error al cargar info de archivo: ${error}';
|
||||
@override String get errorLoadingSeries => 'Error al cargar la serie';
|
||||
@override String get musicNotSupported => 'La reproducción de música aún no está soportada';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesEs extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Toma prestada la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.';
|
||||
@override String get borrowEmpty => 'Nada para tomar prestado todavía.';
|
||||
@override String get borrowEmptySubtitle => 'Conecta Plex o Jellyfin primero a otro perfil.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'De ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Conexión tomada prestada.';
|
||||
@override String get borrowFailed => 'No se pudo tomar prestada la conexión.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvEs extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoritos';
|
||||
@override String get reorderFavorites => 'Reordenar favoritos';
|
||||
@override String get favoritesLoadFailed => 'No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Unirse a sesión en curso';
|
||||
@override String watchFromStart({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)';
|
||||
@override String get watchLive => 'Ver en vivo';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsEs {
|
||||
'auth.quickConnectWaiting' => 'Esperando aprobación…',
|
||||
'auth.quickConnectCancel' => 'Cancelar',
|
||||
'auth.quickConnectExpired' => 'Quick Connect caducó. Inténtalo de nuevo.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Cancelar',
|
||||
'common.save' => 'Guardar',
|
||||
'common.close' => 'Cerrar',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsEs {
|
||||
'settings.libraryDensity' => 'Densidad de Biblioteca',
|
||||
'settings.compact' => 'Compacto',
|
||||
'settings.comfortable' => 'Cómodo',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Modo de Vista',
|
||||
'settings.gridView' => 'Cuadrícula',
|
||||
'settings.listView' => 'Lista',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsEs {
|
||||
'settings.watchTogetherRelay' => 'Relay de Ver Juntos',
|
||||
'settings.watchTogetherRelayDescription' => 'Define un relay personalizado. Todos deben usar el mismo servidor.',
|
||||
'settings.watchTogetherRelayHint' => 'https://mi-relay.ejemplo.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Informes de Errores',
|
||||
'settings.crashReportingDescription' => 'Enviar informes de errores para mejorar la aplicación',
|
||||
'settings.debugLogging' => 'Registro de Depuración',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsEs {
|
||||
'settings.exportSettings' => 'Exportar Configuración',
|
||||
'settings.exportSettingsDescription' => 'Guardar tus preferencias en un archivo',
|
||||
'settings.exportSettingsSuccess' => 'Configuración exportada',
|
||||
'settings.exportSettingsFailed' => 'No se pudo exportar la configuración',
|
||||
'settings.importSettings' => 'Importar Configuración',
|
||||
'settings.importSettingsDescription' => 'Restaurar preferencias desde un archivo',
|
||||
'settings.importSettingsConfirm' => 'Esto reemplazará tu configuración actual. ¿Continuar?',
|
||||
'settings.importSettingsSuccess' => 'Configuración importada',
|
||||
'settings.importSettingsFailed' => 'No se pudo importar la configuración',
|
||||
'settings.importSettingsInvalidFile' => 'Este archivo no es una exportación válida de Plezy',
|
||||
'settings.importSettingsNoUser' => 'Inicia sesión antes de importar la configuración',
|
||||
'settings.shortcutsReset' => 'Atajos restablecidos a los valores predeterminados',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsEs {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'La duración debe estar entre ${min} y ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'El atajo ya está asignado a ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Atajo actualizado para ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Salto automático',
|
||||
'settings.autoSkipIntro' => 'Saltar Intro automáticamente',
|
||||
'settings.autoSkipIntroDescription' => 'Saltar automáticamente los marcadores de intro después de unos segundos',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsEs {
|
||||
'settings.downloadLocationChanged' => 'Ubicación de descarga cambiada',
|
||||
'settings.downloadLocationReset' => 'Ubicación de descarga restablecida al predeterminado',
|
||||
'settings.downloadLocationInvalid' => 'La carpeta seleccionada no tiene permisos de escritura',
|
||||
'settings.downloadLocationSelectError' => 'Error al seleccionar la carpeta',
|
||||
'settings.downloadLocationPickerUnavailable' => 'La selección de carpetas no está disponible en este dispositivo',
|
||||
'settings.downloadOnWifiOnly' => 'Descargar solo con WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Evitar descargas cuando se usan datos móviles',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Eliminar descargas vistas automáticamente',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsEs {
|
||||
'accessibility.hexColor' => 'Color hexadecimal',
|
||||
'accessibility.expandText' => 'Expandir texto',
|
||||
'accessibility.collapseText' => 'Contraer texto',
|
||||
'accessibility.alphabetNavigation' => 'Navegación alfabética',
|
||||
'accessibility.alphabetScrollHint' => 'Desliza hacia arriba o abajo para avanzar por letra',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Fila ${row} de ${rowCount}, columna ${column} de ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Fila ${row} de ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Reproducción aleatoria',
|
||||
'tooltips.playTrailer' => 'Reproducir tráiler',
|
||||
'tooltips.markAsWatched' => 'Marcar como visto',
|
||||
'tooltips.markAsUnwatched' => 'Marcar como no visto',
|
||||
'audioTracks.track' => ({required Object n}) => 'Pista de audio ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Subtítulos',
|
||||
'videoControls.resetToZero' => 'Restablecer a 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsEs {
|
||||
'messages.markedAsUnwatched' => 'Marcado como no visto',
|
||||
'messages.markedAsWatchedOffline' => 'Marcado como visto (se sincronizará al estar en línea)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Marcado como no visto (se sincronizará al estar en línea)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Eliminado automáticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, one: 'Se eliminó automáticamente ${n} descarga vista', other: 'Se eliminaron automáticamente ${n} descargas vistas', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Eliminado automáticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, one: 'Se eliminó automáticamente ${n} descarga vista', other: 'Se eliminaron automáticamente ${n} descargas vistas', ),
|
||||
'messages.removedFromContinueWatching' => 'Eliminado de Seguir Viendo',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Error: ${error}',
|
||||
'messages.streamInterrupted' => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.',
|
||||
'messages.liveStreamInterrupted' => 'La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.',
|
||||
'messages.fileInfoNotAvailable' => 'Información de archivo no disponible',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error al cargar info de archivo: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Error al cargar la serie',
|
||||
'messages.musicNotSupported' => 'La reproducción de música aún no está soportada',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsEs {
|
||||
'profiles.borrowExplain' => 'Toma prestada la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.',
|
||||
'profiles.borrowEmpty' => 'Nada para tomar prestado todavía.',
|
||||
'profiles.borrowEmptySubtitle' => 'Conecta Plex o Jellyfin primero a otro perfil.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'De ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Conexión tomada prestada.',
|
||||
'profiles.borrowFailed' => 'No se pudo tomar prestada la conexión.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsEs {
|
||||
'liveTv.favorites' => 'Favoritos',
|
||||
'liveTv.reorderFavorites' => 'Reordenar favoritos',
|
||||
'liveTv.favoritesLoadFailed' => 'No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Unirse a sesión en curso',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)',
|
||||
'liveTv.watchLive' => 'Ver en vivo',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsEs {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} se fue',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} pausó',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} reanudó',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} avanzó',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} está cargando',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} usa una versión anterior de la app — sincronización no disponible',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Reanudando sin ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Esperando a que otros carguen...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Esperando a ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsFr extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetFr rateSheet = _TranslationsRateSheetFr._(_root);
|
||||
@override late final _TranslationsAccessibilityFr accessibility = _TranslationsAccessibilityFr._(_root);
|
||||
@override late final _TranslationsTooltipsFr tooltips = _TranslationsTooltipsFr._(_root);
|
||||
@override late final _TranslationsAudioTracksFr audioTracks = _TranslationsAudioTracksFr._(_root);
|
||||
@override late final _TranslationsVideoControlsFr videoControls = _TranslationsVideoControlsFr._(_root);
|
||||
@override late final _TranslationsMessagesFr messages = _TranslationsMessagesFr._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingFr subtitlingStyling = _TranslationsSubtitlingStylingFr._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthFr extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'En attente d\'approbation…';
|
||||
@override String get quickConnectCancel => 'Annuler';
|
||||
@override String get quickConnectExpired => 'Quick Connect a expiré. Réessayez.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Densité des bibliothèques';
|
||||
@override String get compact => 'Compact';
|
||||
@override String get comfortable => 'Confortable';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Mode d\'affichage';
|
||||
@override String get gridView => 'Grille';
|
||||
@override String get listView => 'Liste';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relais Regarder Ensemble';
|
||||
@override String get watchTogetherRelayDescription => 'Définir un relay personnalisé. Tout le monde doit utiliser le même serveur.';
|
||||
@override String get watchTogetherRelayHint => 'https://mon-relais.exemple.fr';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Rapports de plantage';
|
||||
@override String get crashReportingDescription => 'Envoyer des rapports de plantage pour améliorer l\'application';
|
||||
@override String get debugLogging => 'Journalisation de débogage';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Exporter les paramètres';
|
||||
@override String get exportSettingsDescription => 'Enregistrer vos préférences dans un fichier';
|
||||
@override String get exportSettingsSuccess => 'Paramètres exportés';
|
||||
@override String get exportSettingsFailed => 'Impossible d\'exporter les paramètres';
|
||||
@override String get importSettings => 'Importer les paramètres';
|
||||
@override String get importSettingsDescription => 'Restaurer les préférences depuis un fichier';
|
||||
@override String get importSettingsConfirm => 'Cela remplacera vos paramètres actuels. Continuer ?';
|
||||
@override String get importSettingsSuccess => 'Paramètres importés';
|
||||
@override String get importSettingsFailed => 'Impossible d\'importer les paramètres';
|
||||
@override String get importSettingsInvalidFile => 'Ce fichier n\'est pas un export Plezy valide';
|
||||
@override String get importSettingsNoUser => 'Connectez-vous avant d\'importer les paramètres';
|
||||
@override String get shortcutsReset => 'Raccourcis réinitialisés aux valeurs par défaut';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'La durée doit être comprise entre ${min} et ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Raccourci déjà attribué à ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Raccourci mis à jour pour ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Skip automatique';
|
||||
@override String get autoSkipIntro => 'Skip automatique de l\'introduction';
|
||||
@override String get autoSkipIntroDescription => 'Skipper automatiquement l\'introduction après quelques secondes';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Emplacement de téléchargement modifié';
|
||||
@override String get downloadLocationReset => 'Emplacement de téléchargement réinitialisé à la valeur par défaut';
|
||||
@override String get downloadLocationInvalid => 'Le dossier sélectionné n\'est pas accessible en écriture';
|
||||
@override String get downloadLocationSelectError => 'Échec de la sélection du dossier';
|
||||
@override String get downloadLocationPickerUnavailable => 'La sélection de dossier n’est pas disponible sur cet appareil';
|
||||
@override String get downloadOnWifiOnly => 'Télécharger uniquement via WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Empêcher les téléchargements lorsque vous utilisez les données cellulaires';
|
||||
@override String get autoRemoveWatchedDownloads => 'Supprimer automatiquement les téléchargements vus';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityFr extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Couleur hexadécimale';
|
||||
@override String get expandText => 'Développer le texte';
|
||||
@override String get collapseText => 'Replier le texte';
|
||||
@override String get alphabetNavigation => 'Navigation alphabétique';
|
||||
@override String get alphabetScrollHint => 'Balayez vers le haut ou le bas pour changer de lettre';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Ligne ${row} sur ${rowCount}, colonne ${column} sur ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Ligne ${row} sur ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsFr extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Marqué comme non vu';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksFr extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksFr._(TranslationsFr root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsFr _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Piste audio ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsFr extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsFr._(TranslationsFr root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesFr extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.';
|
||||
@override String get liveStreamInterrupted => 'Le direct a été interrompu. Appuyez sur Lecture pour réessayer.';
|
||||
@override String get fileInfoNotAvailable => 'Informations sur le fichier non disponibles';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}';
|
||||
@override String get errorLoadingSeries => 'Erreur lors du chargement de la série';
|
||||
@override String get musicNotSupported => 'La lecture de musique n\'est pas encore prise en charge';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesFr extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Emprunter la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.';
|
||||
@override String get borrowEmpty => 'Rien à emprunter pour le moment.';
|
||||
@override String get borrowEmptySubtitle => 'Connectez d\'abord Plex ou Jellyfin à un autre profil.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'De ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Connexion empruntée.';
|
||||
@override String get borrowFailed => 'Impossible d\'emprunter la connexion.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvFr extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoris';
|
||||
@override String get reorderFavorites => 'Réorganiser les favoris';
|
||||
@override String get favoritesLoadFailed => 'Impossible de charger les favoris. Vérifiez votre connexion et réessayez.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Rejoindre la session en cours';
|
||||
@override String watchFromStart({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)';
|
||||
@override String get watchLive => 'Regarder en direct';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsFr {
|
||||
'auth.quickConnectWaiting' => 'En attente d\'approbation…',
|
||||
'auth.quickConnectCancel' => 'Annuler',
|
||||
'auth.quickConnectExpired' => 'Quick Connect a expiré. Réessayez.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Annuler',
|
||||
'common.save' => 'Sauvegarder',
|
||||
'common.close' => 'Fermer',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsFr {
|
||||
'settings.libraryDensity' => 'Densité des bibliothèques',
|
||||
'settings.compact' => 'Compact',
|
||||
'settings.comfortable' => 'Confortable',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Mode d\'affichage',
|
||||
'settings.gridView' => 'Grille',
|
||||
'settings.listView' => 'Liste',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsFr {
|
||||
'settings.watchTogetherRelay' => 'Relais Regarder Ensemble',
|
||||
'settings.watchTogetherRelayDescription' => 'Définir un relay personnalisé. Tout le monde doit utiliser le même serveur.',
|
||||
'settings.watchTogetherRelayHint' => 'https://mon-relais.exemple.fr',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Rapports de plantage',
|
||||
'settings.crashReportingDescription' => 'Envoyer des rapports de plantage pour améliorer l\'application',
|
||||
'settings.debugLogging' => 'Journalisation de débogage',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsFr {
|
||||
'settings.exportSettings' => 'Exporter les paramètres',
|
||||
'settings.exportSettingsDescription' => 'Enregistrer vos préférences dans un fichier',
|
||||
'settings.exportSettingsSuccess' => 'Paramètres exportés',
|
||||
'settings.exportSettingsFailed' => 'Impossible d\'exporter les paramètres',
|
||||
'settings.importSettings' => 'Importer les paramètres',
|
||||
'settings.importSettingsDescription' => 'Restaurer les préférences depuis un fichier',
|
||||
'settings.importSettingsConfirm' => 'Cela remplacera vos paramètres actuels. Continuer ?',
|
||||
'settings.importSettingsSuccess' => 'Paramètres importés',
|
||||
'settings.importSettingsFailed' => 'Impossible d\'importer les paramètres',
|
||||
'settings.importSettingsInvalidFile' => 'Ce fichier n\'est pas un export Plezy valide',
|
||||
'settings.importSettingsNoUser' => 'Connectez-vous avant d\'importer les paramètres',
|
||||
'settings.shortcutsReset' => 'Raccourcis réinitialisés aux valeurs par défaut',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsFr {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'La durée doit être comprise entre ${min} et ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Raccourci déjà attribué à ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Raccourci mis à jour pour ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Skip automatique',
|
||||
'settings.autoSkipIntro' => 'Skip automatique de l\'introduction',
|
||||
'settings.autoSkipIntroDescription' => 'Skipper automatiquement l\'introduction après quelques secondes',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsFr {
|
||||
'settings.downloadLocationChanged' => 'Emplacement de téléchargement modifié',
|
||||
'settings.downloadLocationReset' => 'Emplacement de téléchargement réinitialisé à la valeur par défaut',
|
||||
'settings.downloadLocationInvalid' => 'Le dossier sélectionné n\'est pas accessible en écriture',
|
||||
'settings.downloadLocationSelectError' => 'Échec de la sélection du dossier',
|
||||
'settings.downloadLocationPickerUnavailable' => 'La sélection de dossier n’est pas disponible sur cet appareil',
|
||||
'settings.downloadOnWifiOnly' => 'Télécharger uniquement via WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Empêcher les téléchargements lorsque vous utilisez les données cellulaires',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Supprimer automatiquement les téléchargements vus',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsFr {
|
||||
'accessibility.hexColor' => 'Couleur hexadécimale',
|
||||
'accessibility.expandText' => 'Développer le texte',
|
||||
'accessibility.collapseText' => 'Replier le texte',
|
||||
'accessibility.alphabetNavigation' => 'Navigation alphabétique',
|
||||
'accessibility.alphabetScrollHint' => 'Balayez vers le haut ou le bas pour changer de lettre',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Ligne ${row} sur ${rowCount}, colonne ${column} sur ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Ligne ${row} sur ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Lecture aléatoire',
|
||||
'tooltips.playTrailer' => 'Lire la bande-annonce',
|
||||
'tooltips.markAsWatched' => 'Marqué comme vu',
|
||||
'tooltips.markAsUnwatched' => 'Marqué comme non vu',
|
||||
'audioTracks.track' => ({required Object n}) => 'Piste audio ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Sous-titres',
|
||||
'videoControls.resetToZero' => 'Réinitialiser à 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsFr {
|
||||
'messages.markedAsUnwatched' => 'Marqué comme non vu',
|
||||
'messages.markedAsWatchedOffline' => 'Marqué comme vu (se synchronisera lorsque vous serez en ligne)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Marqué comme non vu (sera synchronisé lorsque vous serez en ligne)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Supprimé automatiquement : ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, one: '${n} téléchargement vu supprimé automatiquement', other: '${n} téléchargements vus supprimés automatiquement', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Supprimé automatiquement : ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, one: '${n} téléchargement vu supprimé automatiquement', other: '${n} téléchargements vus supprimés automatiquement', ),
|
||||
'messages.removedFromContinueWatching' => 'Supprimer de "Continuer à regarder"',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Erreur: ${error}',
|
||||
'messages.streamInterrupted' => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.',
|
||||
'messages.liveStreamInterrupted' => 'Le direct a été interrompu. Appuyez sur Lecture pour réessayer.',
|
||||
'messages.fileInfoNotAvailable' => 'Informations sur le fichier non disponibles',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Erreur lors du chargement de la série',
|
||||
'messages.musicNotSupported' => 'La lecture de musique n\'est pas encore prise en charge',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsFr {
|
||||
'profiles.borrowExplain' => 'Emprunter la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.',
|
||||
'profiles.borrowEmpty' => 'Rien à emprunter pour le moment.',
|
||||
'profiles.borrowEmptySubtitle' => 'Connectez d\'abord Plex ou Jellyfin à un autre profil.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'De ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Connexion empruntée.',
|
||||
'profiles.borrowFailed' => 'Impossible d\'emprunter la connexion.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsFr {
|
||||
'liveTv.favorites' => 'Favoris',
|
||||
'liveTv.reorderFavorites' => 'Réorganiser les favoris',
|
||||
'liveTv.favoritesLoadFailed' => 'Impossible de charger les favoris. Vérifiez votre connexion et réessayez.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Rejoindre la session en cours',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)',
|
||||
'liveTv.watchLive' => 'Regarder en direct',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsFr {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} est parti',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} a mis en pause',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} a repris',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} a avancé',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} met en mémoire tampon',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} utilise une ancienne version de l’app — synchronisation indisponible',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Reprise sans ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'En attente du chargement des autres...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'En attente de ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsIt extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetIt rateSheet = _TranslationsRateSheetIt._(_root);
|
||||
@override late final _TranslationsAccessibilityIt accessibility = _TranslationsAccessibilityIt._(_root);
|
||||
@override late final _TranslationsTooltipsIt tooltips = _TranslationsTooltipsIt._(_root);
|
||||
@override late final _TranslationsAudioTracksIt audioTracks = _TranslationsAudioTracksIt._(_root);
|
||||
@override late final _TranslationsVideoControlsIt videoControls = _TranslationsVideoControlsIt._(_root);
|
||||
@override late final _TranslationsMessagesIt messages = _TranslationsMessagesIt._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingIt subtitlingStyling = _TranslationsSubtitlingStylingIt._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthIt extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'In attesa di approvazione…';
|
||||
@override String get quickConnectCancel => 'Annulla';
|
||||
@override String get quickConnectExpired => 'Quick Connect scaduto. Riprova.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Densità libreria';
|
||||
@override String get compact => 'Compatta';
|
||||
@override String get comfortable => 'Comoda';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Modalità di visualizzazione';
|
||||
@override String get gridView => 'Griglia';
|
||||
@override String get listView => 'Elenco';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relay Guarda Insieme';
|
||||
@override String get watchTogetherRelayDescription => 'Imposta un relay personalizzato. Tutti devono usare lo stesso server.';
|
||||
@override String get watchTogetherRelayHint => 'https://mio-relay.esempio.it';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Segnalazione errori';
|
||||
@override String get crashReportingDescription => 'Invia segnalazioni di errori per migliorare l\'app';
|
||||
@override String get debugLogging => 'Log di debug';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Esporta impostazioni';
|
||||
@override String get exportSettingsDescription => 'Salva le tue preferenze in un file';
|
||||
@override String get exportSettingsSuccess => 'Impostazioni esportate';
|
||||
@override String get exportSettingsFailed => 'Impossibile esportare le impostazioni';
|
||||
@override String get importSettings => 'Importa impostazioni';
|
||||
@override String get importSettingsDescription => 'Ripristina le preferenze da un file';
|
||||
@override String get importSettingsConfirm => 'Questa azione sostituirà le impostazioni attuali. Continuare?';
|
||||
@override String get importSettingsSuccess => 'Impostazioni importate';
|
||||
@override String get importSettingsFailed => 'Impossibile importare le impostazioni';
|
||||
@override String get importSettingsInvalidFile => 'Questo file non è un\'esportazione Plezy valida';
|
||||
@override String get importSettingsNoUser => 'Accedi prima di importare le impostazioni';
|
||||
@override String get shortcutsReset => 'Scorciatoie ripristinate alle impostazioni predefinite';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'la durata deve essere compresa tra ${min} e ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Scorciatoia già assegnata a ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Scorciatoia aggiornata per ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Salto Automatico';
|
||||
@override String get autoSkipIntro => 'Salta Intro Automaticamente';
|
||||
@override String get autoSkipIntroDescription => 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Posizione di download modificata';
|
||||
@override String get downloadLocationReset => 'Posizione di download ripristinata a predefinita';
|
||||
@override String get downloadLocationInvalid => 'La cartella selezionata non è scrivibile';
|
||||
@override String get downloadLocationSelectError => 'Impossibile selezionare la cartella';
|
||||
@override String get downloadLocationPickerUnavailable => 'La selezione della cartella non è disponibile su questo dispositivo';
|
||||
@override String get downloadOnWifiOnly => 'Scarica solo con WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Impedisci i download quando si utilizza la rete dati cellulare';
|
||||
@override String get autoRemoveWatchedDownloads => 'Rimuovi automaticamente i download visti';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityIt extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Colore esadecimale';
|
||||
@override String get expandText => 'Espandi il testo';
|
||||
@override String get collapseText => 'Comprimi il testo';
|
||||
@override String get alphabetNavigation => 'Navigazione alfabetica';
|
||||
@override String get alphabetScrollHint => 'Scorri verso l\'alto o il basso per cambiare lettera';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Riga ${row} di ${rowCount}, colonna ${column} di ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Riga ${row} di ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsIt extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Segna come non visto';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksIt extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksIt._(TranslationsIt root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsIt _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Traccia audio ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsIt extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsIt._(TranslationsIt root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesIt extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.';
|
||||
@override String get liveStreamInterrupted => 'La diretta si è interrotta. Premi Riproduci per riprovare.';
|
||||
@override String get fileInfoNotAvailable => 'Informazioni sul file non disponibili';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Errore caricamento informazioni sul file: ${error}';
|
||||
@override String get errorLoadingSeries => 'Errore caricamento serie';
|
||||
@override String get musicNotSupported => 'La riproduzione musicale non è ancora supportata';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesIt extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Prendi in prestito la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.';
|
||||
@override String get borrowEmpty => 'Nulla da prendere in prestito al momento.';
|
||||
@override String get borrowEmptySubtitle => 'Collega prima Plex o Jellyfin a un altro profilo.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Da ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Connessione presa in prestito.';
|
||||
@override String get borrowFailed => 'Impossibile prendere in prestito la connessione.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvIt extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Preferiti';
|
||||
@override String get reorderFavorites => 'Riordina preferiti';
|
||||
@override String get favoritesLoadFailed => 'Impossibile caricare i preferiti. Controlla la connessione e riprova.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Partecipa alla sessione in corso';
|
||||
@override String watchFromStart({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)';
|
||||
@override String get watchLive => 'Guarda in diretta';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsIt {
|
||||
'auth.quickConnectWaiting' => 'In attesa di approvazione…',
|
||||
'auth.quickConnectCancel' => 'Annulla',
|
||||
'auth.quickConnectExpired' => 'Quick Connect scaduto. Riprova.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Cancella',
|
||||
'common.save' => 'Salva',
|
||||
'common.close' => 'Chiudi',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsIt {
|
||||
'settings.libraryDensity' => 'Densità libreria',
|
||||
'settings.compact' => 'Compatta',
|
||||
'settings.comfortable' => 'Comoda',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Modalità di visualizzazione',
|
||||
'settings.gridView' => 'Griglia',
|
||||
'settings.listView' => 'Elenco',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsIt {
|
||||
'settings.watchTogetherRelay' => 'Relay Guarda Insieme',
|
||||
'settings.watchTogetherRelayDescription' => 'Imposta un relay personalizzato. Tutti devono usare lo stesso server.',
|
||||
'settings.watchTogetherRelayHint' => 'https://mio-relay.esempio.it',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Segnalazione errori',
|
||||
'settings.crashReportingDescription' => 'Invia segnalazioni di errori per migliorare l\'app',
|
||||
'settings.debugLogging' => 'Log di debug',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsIt {
|
||||
'settings.exportSettings' => 'Esporta impostazioni',
|
||||
'settings.exportSettingsDescription' => 'Salva le tue preferenze in un file',
|
||||
'settings.exportSettingsSuccess' => 'Impostazioni esportate',
|
||||
'settings.exportSettingsFailed' => 'Impossibile esportare le impostazioni',
|
||||
'settings.importSettings' => 'Importa impostazioni',
|
||||
'settings.importSettingsDescription' => 'Ripristina le preferenze da un file',
|
||||
'settings.importSettingsConfirm' => 'Questa azione sostituirà le impostazioni attuali. Continuare?',
|
||||
'settings.importSettingsSuccess' => 'Impostazioni importate',
|
||||
'settings.importSettingsFailed' => 'Impossibile importare le impostazioni',
|
||||
'settings.importSettingsInvalidFile' => 'Questo file non è un\'esportazione Plezy valida',
|
||||
'settings.importSettingsNoUser' => 'Accedi prima di importare le impostazioni',
|
||||
'settings.shortcutsReset' => 'Scorciatoie ripristinate alle impostazioni predefinite',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsIt {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'la durata deve essere compresa tra ${min} e ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Scorciatoia già assegnata a ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Scorciatoia aggiornata per ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Salto Automatico',
|
||||
'settings.autoSkipIntro' => 'Salta Intro Automaticamente',
|
||||
'settings.autoSkipIntroDescription' => 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsIt {
|
||||
'settings.downloadLocationChanged' => 'Posizione di download modificata',
|
||||
'settings.downloadLocationReset' => 'Posizione di download ripristinata a predefinita',
|
||||
'settings.downloadLocationInvalid' => 'La cartella selezionata non è scrivibile',
|
||||
'settings.downloadLocationSelectError' => 'Impossibile selezionare la cartella',
|
||||
'settings.downloadLocationPickerUnavailable' => 'La selezione della cartella non è disponibile su questo dispositivo',
|
||||
'settings.downloadOnWifiOnly' => 'Scarica solo con WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Impedisci i download quando si utilizza la rete dati cellulare',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Rimuovi automaticamente i download visti',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsIt {
|
||||
'accessibility.hexColor' => 'Colore esadecimale',
|
||||
'accessibility.expandText' => 'Espandi il testo',
|
||||
'accessibility.collapseText' => 'Comprimi il testo',
|
||||
'accessibility.alphabetNavigation' => 'Navigazione alfabetica',
|
||||
'accessibility.alphabetScrollHint' => 'Scorri verso l\'alto o il basso per cambiare lettera',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Riga ${row} di ${rowCount}, colonna ${column} di ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Riga ${row} di ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Riproduzione casuale',
|
||||
'tooltips.playTrailer' => 'Riproduci trailer',
|
||||
'tooltips.markAsWatched' => 'Segna come visto',
|
||||
'tooltips.markAsUnwatched' => 'Segna come non visto',
|
||||
'audioTracks.track' => ({required Object n}) => 'Traccia audio ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Sottotitoli',
|
||||
'videoControls.resetToZero' => 'Riporta a 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsIt {
|
||||
'messages.markedAsUnwatched' => 'Segna come non visto',
|
||||
'messages.markedAsWatchedOffline' => 'Segnato come visto (sincronizzato online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Segnato come non visto (sincronizzato online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Rimosso automaticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('it'))(n, one: 'Rimosso automaticamente ${n} download già visto', other: 'Rimossi automaticamente ${n} download già visti', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Rimosso automaticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('it'))(n, one: 'Rimosso automaticamente ${n} download già visto', other: 'Rimossi automaticamente ${n} download già visti', ),
|
||||
'messages.removedFromContinueWatching' => 'Rimosso da Continua a guardare',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Errore: ${error}',
|
||||
'messages.streamInterrupted' => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.',
|
||||
'messages.liveStreamInterrupted' => 'La diretta si è interrotta. Premi Riproduci per riprovare.',
|
||||
'messages.fileInfoNotAvailable' => 'Informazioni sul file non disponibili',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Errore caricamento informazioni sul file: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Errore caricamento serie',
|
||||
'messages.musicNotSupported' => 'La riproduzione musicale non è ancora supportata',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsIt {
|
||||
'profiles.borrowExplain' => 'Prendi in prestito la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.',
|
||||
'profiles.borrowEmpty' => 'Nulla da prendere in prestito al momento.',
|
||||
'profiles.borrowEmptySubtitle' => 'Collega prima Plex o Jellyfin a un altro profilo.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Da ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Connessione presa in prestito.',
|
||||
'profiles.borrowFailed' => 'Impossibile prendere in prestito la connessione.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsIt {
|
||||
'liveTv.favorites' => 'Preferiti',
|
||||
'liveTv.reorderFavorites' => 'Riordina preferiti',
|
||||
'liveTv.favoritesLoadFailed' => 'Impossibile caricare i preferiti. Controlla la connessione e riprova.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Partecipa alla sessione in corso',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)',
|
||||
'liveTv.watchLive' => 'Guarda in diretta',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsIt {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} se ne è andato',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} ha messo in pausa',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} ha ripreso',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} ha cercato',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} sta caricando',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} usa una versione precedente dell\'app — sincronizzazione non disponibile',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Ripresa senza ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'In attesa che gli altri carichino...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'In attesa di ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsJa extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetJa rateSheet = _TranslationsRateSheetJa._(_root);
|
||||
@override late final _TranslationsAccessibilityJa accessibility = _TranslationsAccessibilityJa._(_root);
|
||||
@override late final _TranslationsTooltipsJa tooltips = _TranslationsTooltipsJa._(_root);
|
||||
@override late final _TranslationsAudioTracksJa audioTracks = _TranslationsAudioTracksJa._(_root);
|
||||
@override late final _TranslationsVideoControlsJa videoControls = _TranslationsVideoControlsJa._(_root);
|
||||
@override late final _TranslationsMessagesJa messages = _TranslationsMessagesJa._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingJa subtitlingStyling = _TranslationsSubtitlingStylingJa._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthJa extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => '承認を待っています…';
|
||||
@override String get quickConnectCancel => 'キャンセル';
|
||||
@override String get quickConnectExpired => 'Quick Connectの有効期限が切れました。もう一度お試しください。';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'ライブラリの密度';
|
||||
@override String get compact => 'コンパクト';
|
||||
@override String get comfortable => 'ゆったり';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => '表示モード';
|
||||
@override String get gridView => 'グリッド';
|
||||
@override String get listView => 'リスト';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => '一緒に視聴リレーサーバー';
|
||||
@override String get watchTogetherRelayDescription => 'カスタムリレーを設定します。全員が同じサーバーを使う必要があります。';
|
||||
@override String get watchTogetherRelayHint => 'https://my-relay.example.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'クラッシュレポート';
|
||||
@override String get crashReportingDescription => 'アプリの改善に役立つクラッシュレポートを送信';
|
||||
@override String get debugLogging => 'デバッグログ';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => '設定をエクスポート';
|
||||
@override String get exportSettingsDescription => '設定をファイルに保存';
|
||||
@override String get exportSettingsSuccess => '設定をエクスポートしました';
|
||||
@override String get exportSettingsFailed => '設定をエクスポートできませんでした';
|
||||
@override String get importSettings => '設定をインポート';
|
||||
@override String get importSettingsDescription => 'ファイルから設定を復元';
|
||||
@override String get importSettingsConfirm => '現在の設定を置き換えます。続行しますか?';
|
||||
@override String get importSettingsSuccess => '設定をインポートしました';
|
||||
@override String get importSettingsFailed => '設定をインポートできませんでした';
|
||||
@override String get importSettingsInvalidFile => 'このファイルは有効なPlezyの設定エクスポートではありません';
|
||||
@override String get importSettingsNoUser => '設定をインポートする前にサインインしてください';
|
||||
@override String get shortcutsReset => 'ショートカットをデフォルトにリセットしました';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => '時間は${min}から${max} ${unit}の間である必要があります';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'ショートカットは既に${action}に割り当てられています';
|
||||
@override String shortcutUpdated({required Object action}) => '${action}のショートカットを更新しました';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => '自動スキップ';
|
||||
@override String get autoSkipIntro => 'イントロを自動スキップ';
|
||||
@override String get autoSkipIntroDescription => '数秒後にイントロマーカーを自動的にスキップ';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'ダウンロード場所を変更しました';
|
||||
@override String get downloadLocationReset => 'ダウンロード場所をデフォルトにリセットしました';
|
||||
@override String get downloadLocationInvalid => '選択したフォルダは書き込みできません';
|
||||
@override String get downloadLocationSelectError => 'フォルダの選択に失敗しました';
|
||||
@override String get downloadLocationPickerUnavailable => 'このデバイスではフォルダを選択できません';
|
||||
@override String get downloadOnWifiOnly => 'WiFiのみでダウンロード';
|
||||
@override String get downloadOnWifiOnlyDescription => 'モバイルデータ通信時のダウンロードを防止';
|
||||
@override String get autoRemoveWatchedDownloads => '視聴済みダウンロードの自動削除';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityJa extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => '16進カラー';
|
||||
@override String get expandText => 'テキストを展開';
|
||||
@override String get collapseText => 'テキストを折りたたむ';
|
||||
@override String get alphabetNavigation => 'アルファベットナビゲーション';
|
||||
@override String get alphabetScrollHint => '上下にスワイプして文字ごとに移動';
|
||||
@override String rowColumnPosition({required Object rowCount, required Object row, required Object columnCount, required Object column}) => '${rowCount}行中${row}行、${columnCount}列中${column}列';
|
||||
@override String rowPosition({required Object rowCount, required Object row}) => '${rowCount}行中${row}行';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsJa extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => '未視聴にする';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksJa extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksJa._(TranslationsJa root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsJa _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => '音声トラック${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsJa extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsJa._(TranslationsJa root) : this._root = root, super.internal(root);
|
||||
@@ -711,11 +723,11 @@ class _TranslationsMessagesJa extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'ストリームが中断されました。再生を押すかシークして再試行してください。';
|
||||
@override String get liveStreamInterrupted => 'ライブストリームが中断されました。再生を押して再試行してください。';
|
||||
@override String get fileInfoNotAvailable => 'ファイル情報が利用できません';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'ファイル情報の読み込みエラー: ${error}';
|
||||
@override String get errorLoadingSeries => 'シリーズの読み込みエラー';
|
||||
@override String get musicNotSupported => '音楽の再生はまだサポートされていません';
|
||||
@@ -866,6 +878,7 @@ class _TranslationsProfilesJa extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => '別のプロフィールの接続を借用します。PIN保護されたプロフィールにはPINが必要です。';
|
||||
@override String get borrowEmpty => 'まだ借りるものがありません。';
|
||||
@override String get borrowEmptySubtitle => 'まず別のプロフィールにPlexまたはJellyfinを接続してください。';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => '${displayName}から';
|
||||
@override String get borrowConnectionBorrowed => '接続を借用しました。';
|
||||
@override String get borrowFailed => '接続を借用できませんでした。';
|
||||
@@ -1148,7 +1161,7 @@ class _TranslationsLiveTvJa extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'お気に入り';
|
||||
@override String get reorderFavorites => 'お気に入りを並べ替え';
|
||||
@override String get favoritesLoadFailed => 'お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => '進行中のセッションに参加';
|
||||
@override String watchFromStart({required Object minutes}) => '最初から視聴(${minutes}分前に開始)';
|
||||
@override String get watchLive => 'ライブで視聴';
|
||||
@@ -2146,7 +2159,7 @@ extension on TranslationsJa {
|
||||
'auth.quickConnectWaiting' => '承認を待っています…',
|
||||
'auth.quickConnectCancel' => 'キャンセル',
|
||||
'auth.quickConnectExpired' => 'Quick Connectの有効期限が切れました。もう一度お試しください。',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'キャンセル',
|
||||
'common.save' => '保存',
|
||||
'common.close' => '閉じる',
|
||||
@@ -2230,8 +2243,6 @@ extension on TranslationsJa {
|
||||
'settings.libraryDensity' => 'ライブラリの密度',
|
||||
'settings.compact' => 'コンパクト',
|
||||
'settings.comfortable' => 'ゆったり',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => '表示モード',
|
||||
'settings.gridView' => 'グリッド',
|
||||
'settings.listView' => 'リスト',
|
||||
@@ -2301,7 +2312,7 @@ extension on TranslationsJa {
|
||||
'settings.watchTogetherRelay' => '一緒に視聴リレーサーバー',
|
||||
'settings.watchTogetherRelayDescription' => 'カスタムリレーを設定します。全員が同じサーバーを使う必要があります。',
|
||||
'settings.watchTogetherRelayHint' => 'https://my-relay.example.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'クラッシュレポート',
|
||||
'settings.crashReportingDescription' => 'アプリの改善に役立つクラッシュレポートを送信',
|
||||
'settings.debugLogging' => 'デバッグログ',
|
||||
@@ -2318,12 +2329,10 @@ extension on TranslationsJa {
|
||||
'settings.exportSettings' => '設定をエクスポート',
|
||||
'settings.exportSettingsDescription' => '設定をファイルに保存',
|
||||
'settings.exportSettingsSuccess' => '設定をエクスポートしました',
|
||||
'settings.exportSettingsFailed' => '設定をエクスポートできませんでした',
|
||||
'settings.importSettings' => '設定をインポート',
|
||||
'settings.importSettingsDescription' => 'ファイルから設定を復元',
|
||||
'settings.importSettingsConfirm' => '現在の設定を置き換えます。続行しますか?',
|
||||
'settings.importSettingsSuccess' => '設定をインポートしました',
|
||||
'settings.importSettingsFailed' => '設定をインポートできませんでした',
|
||||
'settings.importSettingsInvalidFile' => 'このファイルは有効なPlezyの設定エクスポートではありません',
|
||||
'settings.importSettingsNoUser' => '設定をインポートする前にサインインしてください',
|
||||
'settings.shortcutsReset' => 'ショートカットをデフォルトにリセットしました',
|
||||
@@ -2338,6 +2347,7 @@ extension on TranslationsJa {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => '時間は${min}から${max} ${unit}の間である必要があります',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'ショートカットは既に${action}に割り当てられています',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => '${action}のショートカットを更新しました',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => '自動スキップ',
|
||||
'settings.autoSkipIntro' => 'イントロを自動スキップ',
|
||||
'settings.autoSkipIntroDescription' => '数秒後にイントロマーカーを自動的にスキップ',
|
||||
@@ -2363,7 +2373,7 @@ extension on TranslationsJa {
|
||||
'settings.downloadLocationChanged' => 'ダウンロード場所を変更しました',
|
||||
'settings.downloadLocationReset' => 'ダウンロード場所をデフォルトにリセットしました',
|
||||
'settings.downloadLocationInvalid' => '選択したフォルダは書き込みできません',
|
||||
'settings.downloadLocationSelectError' => 'フォルダの選択に失敗しました',
|
||||
'settings.downloadLocationPickerUnavailable' => 'このデバイスではフォルダを選択できません',
|
||||
'settings.downloadOnWifiOnly' => 'WiFiのみでダウンロード',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'モバイルデータ通信時のダウンロードを防止',
|
||||
'settings.autoRemoveWatchedDownloads' => '視聴済みダウンロードの自動削除',
|
||||
@@ -2551,10 +2561,15 @@ extension on TranslationsJa {
|
||||
'accessibility.hexColor' => '16進カラー',
|
||||
'accessibility.expandText' => 'テキストを展開',
|
||||
'accessibility.collapseText' => 'テキストを折りたたむ',
|
||||
'accessibility.alphabetNavigation' => 'アルファベットナビゲーション',
|
||||
'accessibility.alphabetScrollHint' => '上下にスワイプして文字ごとに移動',
|
||||
'accessibility.rowColumnPosition' => ({required Object rowCount, required Object row, required Object columnCount, required Object column}) => '${rowCount}行中${row}行、${columnCount}列中${column}列',
|
||||
'accessibility.rowPosition' => ({required Object rowCount, required Object row}) => '${rowCount}行中${row}行',
|
||||
'tooltips.shufflePlay' => 'シャッフル再生',
|
||||
'tooltips.playTrailer' => '予告編を再生',
|
||||
'tooltips.markAsWatched' => '視聴済みにする',
|
||||
'tooltips.markAsUnwatched' => '未視聴にする',
|
||||
'audioTracks.track' => ({required Object n}) => '音声トラック${n}',
|
||||
'videoControls.audioLabel' => '音声',
|
||||
'videoControls.subtitlesLabel' => '字幕',
|
||||
'videoControls.resetToZero' => '0msにリセット',
|
||||
@@ -2641,20 +2656,20 @@ extension on TranslationsJa {
|
||||
'messages.markedAsUnwatched' => '未視聴にしました',
|
||||
'messages.markedAsWatchedOffline' => '視聴済みにしました(オンライン時に同期)',
|
||||
'messages.markedAsUnwatchedOffline' => '未視聴にしました(オンライン時に同期)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '自動削除: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ja'))(n, other: '視聴済みダウンロードを${n}件自動削除しました', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '自動削除: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ja'))(n, other: '視聴済みダウンロードを${n}件自動削除しました', ),
|
||||
'messages.removedFromContinueWatching' => '視聴中から削除しました',
|
||||
'messages.errorLoading' => ({required Object error}) => 'エラー: ${error}',
|
||||
'messages.streamInterrupted' => 'ストリームが中断されました。再生を押すかシークして再試行してください。',
|
||||
'messages.liveStreamInterrupted' => 'ライブストリームが中断されました。再生を押して再試行してください。',
|
||||
'messages.fileInfoNotAvailable' => 'ファイル情報が利用できません',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'ファイル情報の読み込みエラー: ${error}',
|
||||
'messages.errorLoadingSeries' => 'シリーズの読み込みエラー',
|
||||
'messages.musicNotSupported' => '音楽の再生はまだサポートされていません',
|
||||
@@ -2769,6 +2784,7 @@ extension on TranslationsJa {
|
||||
'profiles.borrowExplain' => '別のプロフィールの接続を借用します。PIN保護されたプロフィールにはPINが必要です。',
|
||||
'profiles.borrowEmpty' => 'まだ借りるものがありません。',
|
||||
'profiles.borrowEmptySubtitle' => 'まず別のプロフィールにPlexまたはJellyfinを接続してください。',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => '${displayName}から',
|
||||
'profiles.borrowConnectionBorrowed' => '接続を借用しました。',
|
||||
'profiles.borrowFailed' => '接続を借用できませんでした。',
|
||||
@@ -2994,7 +3010,7 @@ extension on TranslationsJa {
|
||||
'liveTv.favorites' => 'お気に入り',
|
||||
'liveTv.reorderFavorites' => 'お気に入りを並べ替え',
|
||||
'liveTv.favoritesLoadFailed' => 'お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => '進行中のセッションに参加',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => '最初から視聴(${minutes}分前に開始)',
|
||||
'liveTv.watchLive' => 'ライブで視聴',
|
||||
@@ -3154,11 +3170,11 @@ extension on TranslationsJa {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name}が退出しました',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name}が一時停止しました',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name}が再開しました',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name}がシークしました',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name}がバッファリング中',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} は古いバージョンのアプリを使用しているため、同期できません',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => '${name} なしで再開',
|
||||
'watchTogether.waitingForParticipants' => '他の参加者の読み込みを待っています...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => '${name}を待っています...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsKo extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetKo rateSheet = _TranslationsRateSheetKo._(_root);
|
||||
@override late final _TranslationsAccessibilityKo accessibility = _TranslationsAccessibilityKo._(_root);
|
||||
@override late final _TranslationsTooltipsKo tooltips = _TranslationsTooltipsKo._(_root);
|
||||
@override late final _TranslationsAudioTracksKo audioTracks = _TranslationsAudioTracksKo._(_root);
|
||||
@override late final _TranslationsVideoControlsKo videoControls = _TranslationsVideoControlsKo._(_root);
|
||||
@override late final _TranslationsMessagesKo messages = _TranslationsMessagesKo._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingKo subtitlingStyling = _TranslationsSubtitlingStylingKo._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthKo extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => '승인 대기 중…';
|
||||
@override String get quickConnectCancel => '취소';
|
||||
@override String get quickConnectExpired => 'Quick Connect가 만료되었습니다. 다시 시도하세요.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => '라이브러리 표시 밀도';
|
||||
@override String get compact => '좁게';
|
||||
@override String get comfortable => '넓게';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => '보기 모드';
|
||||
@override String get gridView => '그리드 보기';
|
||||
@override String get listView => '목록 보기';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => '함께 보기 릴레이';
|
||||
@override String get watchTogetherRelayDescription => '사용자 지정 릴레이를 설정합니다. 모두 같은 서버를 사용해야 합니다.';
|
||||
@override String get watchTogetherRelayHint => 'https://my-relay.example.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => '충돌 보고';
|
||||
@override String get crashReportingDescription => '앱 개선을 위해 충돌 보고서 전송';
|
||||
@override String get debugLogging => '디버그 로깅';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => '설정 내보내기';
|
||||
@override String get exportSettingsDescription => '기본 설정을 파일로 저장';
|
||||
@override String get exportSettingsSuccess => '설정 내보내기 완료';
|
||||
@override String get exportSettingsFailed => '설정을 내보낼 수 없습니다';
|
||||
@override String get importSettings => '설정 가져오기';
|
||||
@override String get importSettingsDescription => '파일에서 기본 설정 복원';
|
||||
@override String get importSettingsConfirm => '현재 설정을 대체합니다. 계속하시겠습니까?';
|
||||
@override String get importSettingsSuccess => '설정 가져오기 완료';
|
||||
@override String get importSettingsFailed => '설정을 가져올 수 없습니다';
|
||||
@override String get importSettingsInvalidFile => '유효한 Plezy 설정 내보내기 파일이 아닙니다';
|
||||
@override String get importSettingsNoUser => '설정을 가져오기 전에 로그인하세요';
|
||||
@override String get shortcutsReset => '단축키가 기본값으로 재설정되었습니다';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => '기간은 ${min}과 ${max} ${unit} 사이여야 합니다';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => '단축키가 이미 ${action}에 할당 되었습니다';
|
||||
@override String shortcutUpdated({required Object action}) => '단축키가 ${action}에 대해 업데이트 되었습니다';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => '자동 건너뛰기';
|
||||
@override String get autoSkipIntro => '자동으로 오프닝 건너뛰기';
|
||||
@override String get autoSkipIntroDescription => '몇 초 후 오프닝을 자동으로 건너뛰기';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => '다운로드 위치가 변경 되었습니다';
|
||||
@override String get downloadLocationReset => '다운로드 위치가 기본값으로 재설정 되었습니다';
|
||||
@override String get downloadLocationInvalid => '선택한 폴더에 쓰기 권한이 없습니다';
|
||||
@override String get downloadLocationSelectError => '폴더 선택 실패';
|
||||
@override String get downloadLocationPickerUnavailable => '이 기기에서는 폴더를 선택할 수 없습니다';
|
||||
@override String get downloadOnWifiOnly => 'WiFi 연결 시에만 다운로드';
|
||||
@override String get downloadOnWifiOnlyDescription => '셀룰러 데이터 사용 시 다운로드 불가';
|
||||
@override String get autoRemoveWatchedDownloads => '시청한 다운로드 자동 삭제';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityKo extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => '16진수 색상';
|
||||
@override String get expandText => '텍스트 펼치기';
|
||||
@override String get collapseText => '텍스트 접기';
|
||||
@override String get alphabetNavigation => '알파벳 탐색';
|
||||
@override String get alphabetScrollHint => '위아래로 스와이프하여 글자별로 이동';
|
||||
@override String rowColumnPosition({required Object rowCount, required Object row, required Object columnCount, required Object column}) => '${rowCount}행 중 ${row}행, ${columnCount}열 중 ${column}열';
|
||||
@override String rowPosition({required Object rowCount, required Object row}) => '${rowCount}행 중 ${row}행';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsKo extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => '시청 안 함으로 표시';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksKo extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksKo._(TranslationsKo root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsKo _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => '오디오 트랙 ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsKo extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsKo._(TranslationsKo root) : this._root = root, super.internal(root);
|
||||
@@ -711,11 +723,11 @@ class _TranslationsMessagesKo extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.';
|
||||
@override String get liveStreamInterrupted => '라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.';
|
||||
@override String get fileInfoNotAvailable => '파일 정보가 없습니다';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => '파일 정보 로딩 중 오류: ${error}';
|
||||
@override String get errorLoadingSeries => '시리즈 로딩 중 오류';
|
||||
@override String get musicNotSupported => '음악 재생 미지원';
|
||||
@@ -866,6 +878,7 @@ class _TranslationsProfilesKo extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => '다른 프로필의 연결을 빌립니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.';
|
||||
@override String get borrowEmpty => '아직 빌릴 것이 없습니다.';
|
||||
@override String get borrowEmptySubtitle => '먼저 다른 프로필에 Plex 또는 Jellyfin을 연결하세요.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => '${displayName}에서';
|
||||
@override String get borrowConnectionBorrowed => '연결을 빌렸습니다.';
|
||||
@override String get borrowFailed => '연결을 빌리지 못했습니다.';
|
||||
@@ -1148,7 +1161,7 @@ class _TranslationsLiveTvKo extends TranslationsLiveTvEn {
|
||||
@override String get favorites => '즐겨찾기';
|
||||
@override String get reorderFavorites => '즐겨찾기 순서 변경';
|
||||
@override String get favoritesLoadFailed => '즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => '진행 중인 세션 참여';
|
||||
@override String watchFromStart({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)';
|
||||
@override String get watchLive => '실시간 시청';
|
||||
@@ -2146,7 +2159,7 @@ extension on TranslationsKo {
|
||||
'auth.quickConnectWaiting' => '승인 대기 중…',
|
||||
'auth.quickConnectCancel' => '취소',
|
||||
'auth.quickConnectExpired' => 'Quick Connect가 만료되었습니다. 다시 시도하세요.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => '취소',
|
||||
'common.save' => '저장',
|
||||
'common.close' => '닫기',
|
||||
@@ -2230,8 +2243,6 @@ extension on TranslationsKo {
|
||||
'settings.libraryDensity' => '라이브러리 표시 밀도',
|
||||
'settings.compact' => '좁게',
|
||||
'settings.comfortable' => '넓게',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => '보기 모드',
|
||||
'settings.gridView' => '그리드 보기',
|
||||
'settings.listView' => '목록 보기',
|
||||
@@ -2301,7 +2312,7 @@ extension on TranslationsKo {
|
||||
'settings.watchTogetherRelay' => '함께 보기 릴레이',
|
||||
'settings.watchTogetherRelayDescription' => '사용자 지정 릴레이를 설정합니다. 모두 같은 서버를 사용해야 합니다.',
|
||||
'settings.watchTogetherRelayHint' => 'https://my-relay.example.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => '충돌 보고',
|
||||
'settings.crashReportingDescription' => '앱 개선을 위해 충돌 보고서 전송',
|
||||
'settings.debugLogging' => '디버그 로깅',
|
||||
@@ -2318,12 +2329,10 @@ extension on TranslationsKo {
|
||||
'settings.exportSettings' => '설정 내보내기',
|
||||
'settings.exportSettingsDescription' => '기본 설정을 파일로 저장',
|
||||
'settings.exportSettingsSuccess' => '설정 내보내기 완료',
|
||||
'settings.exportSettingsFailed' => '설정을 내보낼 수 없습니다',
|
||||
'settings.importSettings' => '설정 가져오기',
|
||||
'settings.importSettingsDescription' => '파일에서 기본 설정 복원',
|
||||
'settings.importSettingsConfirm' => '현재 설정을 대체합니다. 계속하시겠습니까?',
|
||||
'settings.importSettingsSuccess' => '설정 가져오기 완료',
|
||||
'settings.importSettingsFailed' => '설정을 가져올 수 없습니다',
|
||||
'settings.importSettingsInvalidFile' => '유효한 Plezy 설정 내보내기 파일이 아닙니다',
|
||||
'settings.importSettingsNoUser' => '설정을 가져오기 전에 로그인하세요',
|
||||
'settings.shortcutsReset' => '단축키가 기본값으로 재설정되었습니다',
|
||||
@@ -2338,6 +2347,7 @@ extension on TranslationsKo {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => '기간은 ${min}과 ${max} ${unit} 사이여야 합니다',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => '단축키가 이미 ${action}에 할당 되었습니다',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => '단축키가 ${action}에 대해 업데이트 되었습니다',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => '자동 건너뛰기',
|
||||
'settings.autoSkipIntro' => '자동으로 오프닝 건너뛰기',
|
||||
'settings.autoSkipIntroDescription' => '몇 초 후 오프닝을 자동으로 건너뛰기',
|
||||
@@ -2363,7 +2373,7 @@ extension on TranslationsKo {
|
||||
'settings.downloadLocationChanged' => '다운로드 위치가 변경 되었습니다',
|
||||
'settings.downloadLocationReset' => '다운로드 위치가 기본값으로 재설정 되었습니다',
|
||||
'settings.downloadLocationInvalid' => '선택한 폴더에 쓰기 권한이 없습니다',
|
||||
'settings.downloadLocationSelectError' => '폴더 선택 실패',
|
||||
'settings.downloadLocationPickerUnavailable' => '이 기기에서는 폴더를 선택할 수 없습니다',
|
||||
'settings.downloadOnWifiOnly' => 'WiFi 연결 시에만 다운로드',
|
||||
'settings.downloadOnWifiOnlyDescription' => '셀룰러 데이터 사용 시 다운로드 불가',
|
||||
'settings.autoRemoveWatchedDownloads' => '시청한 다운로드 자동 삭제',
|
||||
@@ -2551,10 +2561,15 @@ extension on TranslationsKo {
|
||||
'accessibility.hexColor' => '16진수 색상',
|
||||
'accessibility.expandText' => '텍스트 펼치기',
|
||||
'accessibility.collapseText' => '텍스트 접기',
|
||||
'accessibility.alphabetNavigation' => '알파벳 탐색',
|
||||
'accessibility.alphabetScrollHint' => '위아래로 스와이프하여 글자별로 이동',
|
||||
'accessibility.rowColumnPosition' => ({required Object rowCount, required Object row, required Object columnCount, required Object column}) => '${rowCount}행 중 ${row}행, ${columnCount}열 중 ${column}열',
|
||||
'accessibility.rowPosition' => ({required Object rowCount, required Object row}) => '${rowCount}행 중 ${row}행',
|
||||
'tooltips.shufflePlay' => '무작위 재생',
|
||||
'tooltips.playTrailer' => '예고편 재생',
|
||||
'tooltips.markAsWatched' => '시청 완료로 표시',
|
||||
'tooltips.markAsUnwatched' => '시청 안 함으로 표시',
|
||||
'audioTracks.track' => ({required Object n}) => '오디오 트랙 ${n}',
|
||||
'videoControls.audioLabel' => '오디오',
|
||||
'videoControls.subtitlesLabel' => '자막',
|
||||
'videoControls.resetToZero' => '0ms로 재설정',
|
||||
@@ -2641,20 +2656,20 @@ extension on TranslationsKo {
|
||||
'messages.markedAsUnwatched' => '시청 안 함으로 표시됨',
|
||||
'messages.markedAsWatchedOffline' => '시청 완료로 표시됨 (연결 시 동기화됨)',
|
||||
'messages.markedAsUnwatchedOffline' => '미시청으로 표시됨 (연결 시 동기화됨)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '자동 삭제됨: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n, other: '시청한 다운로드 ${n}개를 자동 삭제했습니다', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '자동 삭제됨: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n, other: '시청한 다운로드 ${n}개를 자동 삭제했습니다', ),
|
||||
'messages.removedFromContinueWatching' => '계속 시청 목록에서 제거됨',
|
||||
'messages.errorLoading' => ({required Object error}) => '오류: ${error}',
|
||||
'messages.streamInterrupted' => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.',
|
||||
'messages.liveStreamInterrupted' => '라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.',
|
||||
'messages.fileInfoNotAvailable' => '파일 정보가 없습니다',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => '파일 정보 로딩 중 오류: ${error}',
|
||||
'messages.errorLoadingSeries' => '시리즈 로딩 중 오류',
|
||||
'messages.musicNotSupported' => '음악 재생 미지원',
|
||||
@@ -2769,6 +2784,7 @@ extension on TranslationsKo {
|
||||
'profiles.borrowExplain' => '다른 프로필의 연결을 빌립니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.',
|
||||
'profiles.borrowEmpty' => '아직 빌릴 것이 없습니다.',
|
||||
'profiles.borrowEmptySubtitle' => '먼저 다른 프로필에 Plex 또는 Jellyfin을 연결하세요.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => '${displayName}에서',
|
||||
'profiles.borrowConnectionBorrowed' => '연결을 빌렸습니다.',
|
||||
'profiles.borrowFailed' => '연결을 빌리지 못했습니다.',
|
||||
@@ -2994,7 +3010,7 @@ extension on TranslationsKo {
|
||||
'liveTv.favorites' => '즐겨찾기',
|
||||
'liveTv.reorderFavorites' => '즐겨찾기 순서 변경',
|
||||
'liveTv.favoritesLoadFailed' => '즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => '진행 중인 세션 참여',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)',
|
||||
'liveTv.watchLive' => '실시간 시청',
|
||||
@@ -3154,11 +3170,11 @@ extension on TranslationsKo {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name}님이 나갔습니다',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name}님이 일시정지했습니다',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name}님이 재생했습니다',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name}님이 탐색했습니다',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name}님이 버퍼링 중입니다',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name}님이 이전 버전의 앱을 사용 중입니다 — 동기화를 사용할 수 없습니다',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => '${name}님 없이 재생을 재개합니다',
|
||||
'watchTogether.waitingForParticipants' => '다른 참가자의 로딩을 기다리는 중...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => '${name}님을 기다리는 중...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsNb extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetNb rateSheet = _TranslationsRateSheetNb._(_root);
|
||||
@override late final _TranslationsAccessibilityNb accessibility = _TranslationsAccessibilityNb._(_root);
|
||||
@override late final _TranslationsTooltipsNb tooltips = _TranslationsTooltipsNb._(_root);
|
||||
@override late final _TranslationsAudioTracksNb audioTracks = _TranslationsAudioTracksNb._(_root);
|
||||
@override late final _TranslationsVideoControlsNb videoControls = _TranslationsVideoControlsNb._(_root);
|
||||
@override late final _TranslationsMessagesNb messages = _TranslationsMessagesNb._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingNb subtitlingStyling = _TranslationsSubtitlingStylingNb._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthNb extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Venter på godkjenning…';
|
||||
@override String get quickConnectCancel => 'Avbryt';
|
||||
@override String get quickConnectExpired => 'Quick Connect er utløpt. Prøv igjen.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Bibliotekets tetthet';
|
||||
@override String get compact => 'Kompakt';
|
||||
@override String get comfortable => 'Komfortabel';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Visningsmodus';
|
||||
@override String get gridView => 'Rutenett';
|
||||
@override String get listView => 'Liste';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Se Sammen-relay';
|
||||
@override String get watchTogetherRelayDescription => 'Angi en egendefinert relay. Alle må bruke samme server.';
|
||||
@override String get watchTogetherRelayHint => 'https://min-relay.eksempel.no';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Krasjrapportering';
|
||||
@override String get crashReportingDescription => 'Send krasjrapporter for å hjelpe med å forbedre appen';
|
||||
@override String get debugLogging => 'Feilsøkingslogging';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Eksporter innstillinger';
|
||||
@override String get exportSettingsDescription => 'Lagre innstillingene i en fil';
|
||||
@override String get exportSettingsSuccess => 'Innstillinger eksportert';
|
||||
@override String get exportSettingsFailed => 'Kunne ikke eksportere innstillinger';
|
||||
@override String get importSettings => 'Importer innstillinger';
|
||||
@override String get importSettingsDescription => 'Gjenopprett innstillinger fra en fil';
|
||||
@override String get importSettingsConfirm => 'Dette vil erstatte nåværende innstillinger. Fortsette?';
|
||||
@override String get importSettingsSuccess => 'Innstillinger importert';
|
||||
@override String get importSettingsFailed => 'Kunne ikke importere innstillinger';
|
||||
@override String get importSettingsInvalidFile => 'Denne filen er ikke en gyldig Plezy-innstillingseksport';
|
||||
@override String get importSettingsNoUser => 'Logg inn før import av innstillinger';
|
||||
@override String get shortcutsReset => 'Snarveier tilbakestilt til standard';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Varigheten må være mellom ${min} og ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Snarvei allerede tilordnet til ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Snarvei oppdatert for ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Automatisk hopp';
|
||||
@override String get autoSkipIntro => 'Hopp over intro automatisk';
|
||||
@override String get autoSkipIntroDescription => 'Hopp automatisk over intromarkører etter noen sekunder';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Nedlastingsplassering endret';
|
||||
@override String get downloadLocationReset => 'Nedlastingsplassering tilbakestilt til standard';
|
||||
@override String get downloadLocationInvalid => 'Valgt mappe er ikke skrivbar';
|
||||
@override String get downloadLocationSelectError => 'Kunne ikke velge mappe';
|
||||
@override String get downloadLocationPickerUnavailable => 'Mappevalg er ikke tilgjengelig på denne enheten';
|
||||
@override String get downloadOnWifiOnly => 'Last ned kun på WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Forhindre nedlastinger på mobildata';
|
||||
@override String get autoRemoveWatchedDownloads => 'Fjern sette nedlastinger automatisk';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityNb extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Heksadesimal farge';
|
||||
@override String get expandText => 'Utvid tekst';
|
||||
@override String get collapseText => 'Fold sammen tekst';
|
||||
@override String get alphabetNavigation => 'Alfabetisk navigasjon';
|
||||
@override String get alphabetScrollHint => 'Sveip opp eller ned for å flytte én bokstav';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rad ${row} av ${rowCount}, kolonne ${column} av ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Rad ${row} av ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsNb extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Merk som usett';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksNb extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksNb._(TranslationsNb root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsNb _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Lydspor ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsNb extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsNb._(TranslationsNb root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesNb extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.';
|
||||
@override String get liveStreamInterrupted => 'Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.';
|
||||
@override String get fileInfoNotAvailable => 'Filinformasjon ikke tilgjengelig';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}';
|
||||
@override String get errorLoadingSeries => 'Feil ved lasting av serie';
|
||||
@override String get musicNotSupported => 'Musikkavspilling støttes ikke ennå';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesNb extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Lån en annen profils tilkobling. PIN-beskyttede profiler krever PIN.';
|
||||
@override String get borrowEmpty => 'Ingenting å låne enda.';
|
||||
@override String get borrowEmptySubtitle => 'Koble Plex eller Jellyfin til en annen profil først.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Fra ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Tilkobling lånt.';
|
||||
@override String get borrowFailed => 'Kunne ikke låne tilkoblingen.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvNb extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoritter';
|
||||
@override String get reorderFavorites => 'Endre rekkefølge på favoritter';
|
||||
@override String get favoritesLoadFailed => 'Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Bli med i pågående økt';
|
||||
@override String watchFromStart({required Object minutes}) => 'Se fra starten (${minutes} min siden)';
|
||||
@override String get watchLive => 'Se direkte';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsNb {
|
||||
'auth.quickConnectWaiting' => 'Venter på godkjenning…',
|
||||
'auth.quickConnectCancel' => 'Avbryt',
|
||||
'auth.quickConnectExpired' => 'Quick Connect er utløpt. Prøv igjen.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Avbryt',
|
||||
'common.save' => 'Lagre',
|
||||
'common.close' => 'Lukk',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsNb {
|
||||
'settings.libraryDensity' => 'Bibliotekets tetthet',
|
||||
'settings.compact' => 'Kompakt',
|
||||
'settings.comfortable' => 'Komfortabel',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Visningsmodus',
|
||||
'settings.gridView' => 'Rutenett',
|
||||
'settings.listView' => 'Liste',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsNb {
|
||||
'settings.watchTogetherRelay' => 'Se Sammen-relay',
|
||||
'settings.watchTogetherRelayDescription' => 'Angi en egendefinert relay. Alle må bruke samme server.',
|
||||
'settings.watchTogetherRelayHint' => 'https://min-relay.eksempel.no',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Krasjrapportering',
|
||||
'settings.crashReportingDescription' => 'Send krasjrapporter for å hjelpe med å forbedre appen',
|
||||
'settings.debugLogging' => 'Feilsøkingslogging',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsNb {
|
||||
'settings.exportSettings' => 'Eksporter innstillinger',
|
||||
'settings.exportSettingsDescription' => 'Lagre innstillingene i en fil',
|
||||
'settings.exportSettingsSuccess' => 'Innstillinger eksportert',
|
||||
'settings.exportSettingsFailed' => 'Kunne ikke eksportere innstillinger',
|
||||
'settings.importSettings' => 'Importer innstillinger',
|
||||
'settings.importSettingsDescription' => 'Gjenopprett innstillinger fra en fil',
|
||||
'settings.importSettingsConfirm' => 'Dette vil erstatte nåværende innstillinger. Fortsette?',
|
||||
'settings.importSettingsSuccess' => 'Innstillinger importert',
|
||||
'settings.importSettingsFailed' => 'Kunne ikke importere innstillinger',
|
||||
'settings.importSettingsInvalidFile' => 'Denne filen er ikke en gyldig Plezy-innstillingseksport',
|
||||
'settings.importSettingsNoUser' => 'Logg inn før import av innstillinger',
|
||||
'settings.shortcutsReset' => 'Snarveier tilbakestilt til standard',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsNb {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Varigheten må være mellom ${min} og ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Snarvei allerede tilordnet til ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Snarvei oppdatert for ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Automatisk hopp',
|
||||
'settings.autoSkipIntro' => 'Hopp over intro automatisk',
|
||||
'settings.autoSkipIntroDescription' => 'Hopp automatisk over intromarkører etter noen sekunder',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsNb {
|
||||
'settings.downloadLocationChanged' => 'Nedlastingsplassering endret',
|
||||
'settings.downloadLocationReset' => 'Nedlastingsplassering tilbakestilt til standard',
|
||||
'settings.downloadLocationInvalid' => 'Valgt mappe er ikke skrivbar',
|
||||
'settings.downloadLocationSelectError' => 'Kunne ikke velge mappe',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Mappevalg er ikke tilgjengelig på denne enheten',
|
||||
'settings.downloadOnWifiOnly' => 'Last ned kun på WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Forhindre nedlastinger på mobildata',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Fjern sette nedlastinger automatisk',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsNb {
|
||||
'accessibility.hexColor' => 'Heksadesimal farge',
|
||||
'accessibility.expandText' => 'Utvid tekst',
|
||||
'accessibility.collapseText' => 'Fold sammen tekst',
|
||||
'accessibility.alphabetNavigation' => 'Alfabetisk navigasjon',
|
||||
'accessibility.alphabetScrollHint' => 'Sveip opp eller ned for å flytte én bokstav',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rad ${row} av ${rowCount}, kolonne ${column} av ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Rad ${row} av ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Tilfeldig avspilling',
|
||||
'tooltips.playTrailer' => 'Spill trailer',
|
||||
'tooltips.markAsWatched' => 'Merk som sett',
|
||||
'tooltips.markAsUnwatched' => 'Merk som usett',
|
||||
'audioTracks.track' => ({required Object n}) => 'Lydspor ${n}',
|
||||
'videoControls.audioLabel' => 'Lyd',
|
||||
'videoControls.subtitlesLabel' => 'Undertekster',
|
||||
'videoControls.resetToZero' => 'Tilbakestill til 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsNb {
|
||||
'messages.markedAsUnwatched' => 'Merket som usett',
|
||||
'messages.markedAsWatchedOffline' => 'Merket som sett (synkroniseres når tilkoblet)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Merket som usett (synkroniseres når tilkoblet)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisk fjernet: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('nb'))(n, one: 'Fjernet automatisk ${n} sett nedlasting', other: 'Fjernet automatisk ${n} sette nedlastinger', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisk fjernet: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('nb'))(n, one: 'Fjernet automatisk ${n} sett nedlasting', other: 'Fjernet automatisk ${n} sette nedlastinger', ),
|
||||
'messages.removedFromContinueWatching' => 'Fjernet fra Fortsett å se',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Feil: ${error}',
|
||||
'messages.streamInterrupted' => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.',
|
||||
'messages.liveStreamInterrupted' => 'Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.',
|
||||
'messages.fileInfoNotAvailable' => 'Filinformasjon ikke tilgjengelig',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Feil ved lasting av serie',
|
||||
'messages.musicNotSupported' => 'Musikkavspilling støttes ikke ennå',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsNb {
|
||||
'profiles.borrowExplain' => 'Lån en annen profils tilkobling. PIN-beskyttede profiler krever PIN.',
|
||||
'profiles.borrowEmpty' => 'Ingenting å låne enda.',
|
||||
'profiles.borrowEmptySubtitle' => 'Koble Plex eller Jellyfin til en annen profil først.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Fra ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Tilkobling lånt.',
|
||||
'profiles.borrowFailed' => 'Kunne ikke låne tilkoblingen.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsNb {
|
||||
'liveTv.favorites' => 'Favoritter',
|
||||
'liveTv.reorderFavorites' => 'Endre rekkefølge på favoritter',
|
||||
'liveTv.favoritesLoadFailed' => 'Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Bli med i pågående økt',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra starten (${minutes} min siden)',
|
||||
'liveTv.watchLive' => 'Se direkte',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsNb {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} forlot',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} pauset',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} gjenopptok',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} spolet',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} buffrer',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} bruker en eldre appversjon — synkronisering er ikke tilgjengelig',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Fortsetter uten ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Venter på at andre laster inn...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Venter på ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsNl extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetNl rateSheet = _TranslationsRateSheetNl._(_root);
|
||||
@override late final _TranslationsAccessibilityNl accessibility = _TranslationsAccessibilityNl._(_root);
|
||||
@override late final _TranslationsTooltipsNl tooltips = _TranslationsTooltipsNl._(_root);
|
||||
@override late final _TranslationsAudioTracksNl audioTracks = _TranslationsAudioTracksNl._(_root);
|
||||
@override late final _TranslationsVideoControlsNl videoControls = _TranslationsVideoControlsNl._(_root);
|
||||
@override late final _TranslationsMessagesNl messages = _TranslationsMessagesNl._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingNl subtitlingStyling = _TranslationsSubtitlingStylingNl._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthNl extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Wachten op goedkeuring…';
|
||||
@override String get quickConnectCancel => 'Annuleren';
|
||||
@override String get quickConnectExpired => 'Quick Connect is verlopen. Probeer opnieuw.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Bibliotheek dichtheid';
|
||||
@override String get compact => 'Compact';
|
||||
@override String get comfortable => 'Comfortabel';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Weergavemodus';
|
||||
@override String get gridView => 'Raster';
|
||||
@override String get listView => 'Lijst';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Samen Kijken Relay';
|
||||
@override String get watchTogetherRelayDescription => 'Stel een aangepaste relay in. Iedereen moet dezelfde server gebruiken.';
|
||||
@override String get watchTogetherRelayHint => 'https://mijn-relay.voorbeeld.nl';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Crashrapportage';
|
||||
@override String get crashReportingDescription => 'Crashrapporten verzenden om de app te verbeteren';
|
||||
@override String get debugLogging => 'Debug logging';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Instellingen exporteren';
|
||||
@override String get exportSettingsDescription => 'Sla je voorkeuren op in een bestand';
|
||||
@override String get exportSettingsSuccess => 'Instellingen geëxporteerd';
|
||||
@override String get exportSettingsFailed => 'Kon instellingen niet exporteren';
|
||||
@override String get importSettings => 'Instellingen importeren';
|
||||
@override String get importSettingsDescription => 'Voorkeuren herstellen vanuit een bestand';
|
||||
@override String get importSettingsConfirm => 'Hiermee worden je huidige instellingen vervangen. Doorgaan?';
|
||||
@override String get importSettingsSuccess => 'Instellingen geïmporteerd';
|
||||
@override String get importSettingsFailed => 'Kon instellingen niet importeren';
|
||||
@override String get importSettingsInvalidFile => 'Dit bestand is geen geldige Plezy-export';
|
||||
@override String get importSettingsNoUser => 'Meld je aan voordat je instellingen importeert';
|
||||
@override String get shortcutsReset => 'Sneltoetsen gereset naar standaard';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Duur moet tussen ${min} en ${max} ${unit} zijn';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Sneltoets al toegewezen aan ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Sneltoets bijgewerkt voor ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Automatisch Overslaan';
|
||||
@override String get autoSkipIntro => 'Intro Automatisch Overslaan';
|
||||
@override String get autoSkipIntroDescription => 'Intro-markeringen na enkele seconden automatisch overslaan';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Downloadlocatie gewijzigd';
|
||||
@override String get downloadLocationReset => 'Downloadlocatie hersteld naar standaard';
|
||||
@override String get downloadLocationInvalid => 'Geselecteerde map is niet beschrijfbaar';
|
||||
@override String get downloadLocationSelectError => 'Kan map niet selecteren';
|
||||
@override String get downloadLocationPickerUnavailable => 'Mapselectie is niet beschikbaar op dit apparaat';
|
||||
@override String get downloadOnWifiOnly => 'Alleen via WiFi downloaden';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Voorkom downloads bij gebruik van mobiele data';
|
||||
@override String get autoRemoveWatchedDownloads => 'Bekeken downloads automatisch verwijderen';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityNl extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Hexkleur';
|
||||
@override String get expandText => 'Tekst uitvouwen';
|
||||
@override String get collapseText => 'Tekst samenvouwen';
|
||||
@override String get alphabetNavigation => 'Alfabetische navigatie';
|
||||
@override String get alphabetScrollHint => 'Veeg omhoog of omlaag om per letter te bewegen';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rij ${row} van ${rowCount}, kolom ${column} van ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Rij ${row} van ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsNl extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Markeer als ongekeken';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksNl extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksNl._(TranslationsNl root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsNl _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Audiospoor ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsNl extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsNl._(TranslationsNl root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesNl extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.';
|
||||
@override String get liveStreamInterrupted => 'De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.';
|
||||
@override String get fileInfoNotAvailable => 'Bestand informatie niet beschikbaar';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Fout bij laden bestand info: ${error}';
|
||||
@override String get errorLoadingSeries => 'Fout bij laden serie';
|
||||
@override String get musicNotSupported => 'Muziek afspelen wordt nog niet ondersteund';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesNl extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Leen de verbinding van een ander profiel. PIN-beveiligde profielen vereisen een PIN.';
|
||||
@override String get borrowEmpty => 'Nog niets te lenen.';
|
||||
@override String get borrowEmptySubtitle => 'Verbind Plex of Jellyfin eerst met een ander profiel.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Van ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Verbinding geleend.';
|
||||
@override String get borrowFailed => 'Kan verbinding niet lenen.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvNl extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favorieten';
|
||||
@override String get reorderFavorites => 'Favorieten herordenen';
|
||||
@override String get favoritesLoadFailed => 'Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Deelnemen aan lopende sessie';
|
||||
@override String watchFromStart({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)';
|
||||
@override String get watchLive => 'Live kijken';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsNl {
|
||||
'auth.quickConnectWaiting' => 'Wachten op goedkeuring…',
|
||||
'auth.quickConnectCancel' => 'Annuleren',
|
||||
'auth.quickConnectExpired' => 'Quick Connect is verlopen. Probeer opnieuw.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Annuleren',
|
||||
'common.save' => 'Opslaan',
|
||||
'common.close' => 'Sluiten',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsNl {
|
||||
'settings.libraryDensity' => 'Bibliotheek dichtheid',
|
||||
'settings.compact' => 'Compact',
|
||||
'settings.comfortable' => 'Comfortabel',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Weergavemodus',
|
||||
'settings.gridView' => 'Raster',
|
||||
'settings.listView' => 'Lijst',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsNl {
|
||||
'settings.watchTogetherRelay' => 'Samen Kijken Relay',
|
||||
'settings.watchTogetherRelayDescription' => 'Stel een aangepaste relay in. Iedereen moet dezelfde server gebruiken.',
|
||||
'settings.watchTogetherRelayHint' => 'https://mijn-relay.voorbeeld.nl',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Crashrapportage',
|
||||
'settings.crashReportingDescription' => 'Crashrapporten verzenden om de app te verbeteren',
|
||||
'settings.debugLogging' => 'Debug logging',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsNl {
|
||||
'settings.exportSettings' => 'Instellingen exporteren',
|
||||
'settings.exportSettingsDescription' => 'Sla je voorkeuren op in een bestand',
|
||||
'settings.exportSettingsSuccess' => 'Instellingen geëxporteerd',
|
||||
'settings.exportSettingsFailed' => 'Kon instellingen niet exporteren',
|
||||
'settings.importSettings' => 'Instellingen importeren',
|
||||
'settings.importSettingsDescription' => 'Voorkeuren herstellen vanuit een bestand',
|
||||
'settings.importSettingsConfirm' => 'Hiermee worden je huidige instellingen vervangen. Doorgaan?',
|
||||
'settings.importSettingsSuccess' => 'Instellingen geïmporteerd',
|
||||
'settings.importSettingsFailed' => 'Kon instellingen niet importeren',
|
||||
'settings.importSettingsInvalidFile' => 'Dit bestand is geen geldige Plezy-export',
|
||||
'settings.importSettingsNoUser' => 'Meld je aan voordat je instellingen importeert',
|
||||
'settings.shortcutsReset' => 'Sneltoetsen gereset naar standaard',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsNl {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Duur moet tussen ${min} en ${max} ${unit} zijn',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Sneltoets al toegewezen aan ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Sneltoets bijgewerkt voor ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Automatisch Overslaan',
|
||||
'settings.autoSkipIntro' => 'Intro Automatisch Overslaan',
|
||||
'settings.autoSkipIntroDescription' => 'Intro-markeringen na enkele seconden automatisch overslaan',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsNl {
|
||||
'settings.downloadLocationChanged' => 'Downloadlocatie gewijzigd',
|
||||
'settings.downloadLocationReset' => 'Downloadlocatie hersteld naar standaard',
|
||||
'settings.downloadLocationInvalid' => 'Geselecteerde map is niet beschrijfbaar',
|
||||
'settings.downloadLocationSelectError' => 'Kan map niet selecteren',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Mapselectie is niet beschikbaar op dit apparaat',
|
||||
'settings.downloadOnWifiOnly' => 'Alleen via WiFi downloaden',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Voorkom downloads bij gebruik van mobiele data',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Bekeken downloads automatisch verwijderen',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsNl {
|
||||
'accessibility.hexColor' => 'Hexkleur',
|
||||
'accessibility.expandText' => 'Tekst uitvouwen',
|
||||
'accessibility.collapseText' => 'Tekst samenvouwen',
|
||||
'accessibility.alphabetNavigation' => 'Alfabetische navigatie',
|
||||
'accessibility.alphabetScrollHint' => 'Veeg omhoog of omlaag om per letter te bewegen',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rij ${row} van ${rowCount}, kolom ${column} van ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Rij ${row} van ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Willekeurig afspelen',
|
||||
'tooltips.playTrailer' => 'Trailer afspelen',
|
||||
'tooltips.markAsWatched' => 'Markeer als gekeken',
|
||||
'tooltips.markAsUnwatched' => 'Markeer als ongekeken',
|
||||
'audioTracks.track' => ({required Object n}) => 'Audiospoor ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Ondertitels',
|
||||
'videoControls.resetToZero' => 'Reset naar 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsNl {
|
||||
'messages.markedAsUnwatched' => 'Gemarkeerd als ongekeken',
|
||||
'messages.markedAsWatchedOffline' => 'Gemarkeerd als gekeken (sync wanneer online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Gemarkeerd als ongekeken (sync wanneer online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisch verwijderd: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('nl'))(n, one: 'Automatisch ${n} bekeken download verwijderd', other: 'Automatisch ${n} bekeken downloads verwijderd', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatisch verwijderd: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('nl'))(n, one: 'Automatisch ${n} bekeken download verwijderd', other: 'Automatisch ${n} bekeken downloads verwijderd', ),
|
||||
'messages.removedFromContinueWatching' => 'Verwijderd uit Doorgaan met kijken',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Fout: ${error}',
|
||||
'messages.streamInterrupted' => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.',
|
||||
'messages.liveStreamInterrupted' => 'De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.',
|
||||
'messages.fileInfoNotAvailable' => 'Bestand informatie niet beschikbaar',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fout bij laden bestand info: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Fout bij laden serie',
|
||||
'messages.musicNotSupported' => 'Muziek afspelen wordt nog niet ondersteund',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsNl {
|
||||
'profiles.borrowExplain' => 'Leen de verbinding van een ander profiel. PIN-beveiligde profielen vereisen een PIN.',
|
||||
'profiles.borrowEmpty' => 'Nog niets te lenen.',
|
||||
'profiles.borrowEmptySubtitle' => 'Verbind Plex of Jellyfin eerst met een ander profiel.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Van ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Verbinding geleend.',
|
||||
'profiles.borrowFailed' => 'Kan verbinding niet lenen.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsNl {
|
||||
'liveTv.favorites' => 'Favorieten',
|
||||
'liveTv.reorderFavorites' => 'Favorieten herordenen',
|
||||
'liveTv.favoritesLoadFailed' => 'Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Deelnemen aan lopende sessie',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)',
|
||||
'liveTv.watchLive' => 'Live kijken',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsNl {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} heeft de sessie verlaten',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} heeft gepauzeerd',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} heeft hervat',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} heeft gespoeld',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} is aan het bufferen',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} gebruikt een oudere appversie — synchronisatie niet beschikbaar',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Hervatten zonder ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Wachten tot anderen geladen zijn...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Wachten op ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsPl extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetPl rateSheet = _TranslationsRateSheetPl._(_root);
|
||||
@override late final _TranslationsAccessibilityPl accessibility = _TranslationsAccessibilityPl._(_root);
|
||||
@override late final _TranslationsTooltipsPl tooltips = _TranslationsTooltipsPl._(_root);
|
||||
@override late final _TranslationsAudioTracksPl audioTracks = _TranslationsAudioTracksPl._(_root);
|
||||
@override late final _TranslationsVideoControlsPl videoControls = _TranslationsVideoControlsPl._(_root);
|
||||
@override late final _TranslationsMessagesPl messages = _TranslationsMessagesPl._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingPl subtitlingStyling = _TranslationsSubtitlingStylingPl._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthPl extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Oczekiwanie na zatwierdzenie…';
|
||||
@override String get quickConnectCancel => 'Anuluj';
|
||||
@override String get quickConnectExpired => 'Quick Connect wygasł. Spróbuj ponownie.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Gęstość biblioteki';
|
||||
@override String get compact => 'Kompaktowy';
|
||||
@override String get comfortable => 'Wygodny';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Tryb widoku';
|
||||
@override String get gridView => 'Siatka';
|
||||
@override String get listView => 'Lista';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relay Oglądaj Razem';
|
||||
@override String get watchTogetherRelayDescription => 'Ustaw własny relay. Wszyscy muszą używać tego samego serwera.';
|
||||
@override String get watchTogetherRelayHint => 'https://moj-relay.przyklad.pl';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Raportowanie błędów';
|
||||
@override String get crashReportingDescription => 'Wysyłaj raporty o błędach, aby pomóc ulepszyć aplikację';
|
||||
@override String get debugLogging => 'Logowanie debugowania';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Eksportuj ustawienia';
|
||||
@override String get exportSettingsDescription => 'Zapisz swoje preferencje do pliku';
|
||||
@override String get exportSettingsSuccess => 'Ustawienia wyeksportowane';
|
||||
@override String get exportSettingsFailed => 'Nie można wyeksportować ustawień';
|
||||
@override String get importSettings => 'Importuj ustawienia';
|
||||
@override String get importSettingsDescription => 'Przywróć preferencje z pliku';
|
||||
@override String get importSettingsConfirm => 'Bieżące ustawienia zostaną zastąpione. Kontynuować?';
|
||||
@override String get importSettingsSuccess => 'Ustawienia zaimportowane';
|
||||
@override String get importSettingsFailed => 'Nie można zaimportować ustawień';
|
||||
@override String get importSettingsInvalidFile => 'Ten plik nie jest prawidłowym eksportem Plezy';
|
||||
@override String get importSettingsNoUser => 'Zaloguj się przed importem ustawień';
|
||||
@override String get shortcutsReset => 'Skróty przywrócone do domyślnych';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Czas musi być między ${min} a ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Skrót jest już przypisany do ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Skrót zaktualizowany dla ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Automatyczne pomijanie';
|
||||
@override String get autoSkipIntro => 'Automatyczne pomijanie intro';
|
||||
@override String get autoSkipIntroDescription => 'Automatycznie pomijaj znaczniki intro po kilku sekundach';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Lokalizacja pobierania zmieniona';
|
||||
@override String get downloadLocationReset => 'Lokalizacja pobierania przywrócona do domyślnej';
|
||||
@override String get downloadLocationInvalid => 'Wybrany folder nie jest zapisywalny';
|
||||
@override String get downloadLocationSelectError => 'Nie udało się wybrać folderu';
|
||||
@override String get downloadLocationPickerUnavailable => 'Wybór folderu nie jest dostępny na tym urządzeniu';
|
||||
@override String get downloadOnWifiOnly => 'Pobieraj tylko przez WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Blokuj pobieranie na danych komórkowych';
|
||||
@override String get autoRemoveWatchedDownloads => 'Automatycznie usuwaj obejrzane pobrania';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityPl extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Kolor szesnastkowy';
|
||||
@override String get expandText => 'Rozwiń tekst';
|
||||
@override String get collapseText => 'Zwiń tekst';
|
||||
@override String get alphabetNavigation => 'Nawigacja alfabetyczna';
|
||||
@override String get alphabetScrollHint => 'Przesuń w górę lub w dół, aby przejść o literę';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Wiersz ${row} z ${rowCount}, kolumna ${column} z ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Wiersz ${row} z ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsPl extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Oznacz jako nieobejrzane';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksPl extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksPl._(TranslationsPl root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsPl _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Ścieżka audio ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsPl extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsPl._(TranslationsPl root) : this._root = root, super.internal(root);
|
||||
@@ -714,11 +726,11 @@ class _TranslationsMessagesPl extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.';
|
||||
@override String get liveStreamInterrupted => 'Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.';
|
||||
@override String get fileInfoNotAvailable => 'Informacje o pliku niedostępne';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}';
|
||||
@override String get errorLoadingSeries => 'Błąd ładowania serialu';
|
||||
@override String get musicNotSupported => 'Odtwarzanie muzyki nie jest jeszcze obsługiwane';
|
||||
@@ -869,6 +881,7 @@ class _TranslationsProfilesPl extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Pożycz połączenie z innego profilu. Profile chronione PIN wymagają PIN-u.';
|
||||
@override String get borrowEmpty => 'Nic do pożyczenia.';
|
||||
@override String get borrowEmptySubtitle => 'Najpierw połącz Plex lub Jellyfin z innym profilem.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Od ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Połączenie pożyczone.';
|
||||
@override String get borrowFailed => 'Nie udało się pożyczyć połączenia.';
|
||||
@@ -1154,7 +1167,7 @@ class _TranslationsLiveTvPl extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Ulubione';
|
||||
@override String get reorderFavorites => 'Zmień kolejność ulubionych';
|
||||
@override String get favoritesLoadFailed => 'Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Dołącz do trwającej sesji';
|
||||
@override String watchFromStart({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)';
|
||||
@override String get watchLive => 'Oglądaj na żywo';
|
||||
@@ -2155,7 +2168,7 @@ extension on TranslationsPl {
|
||||
'auth.quickConnectWaiting' => 'Oczekiwanie na zatwierdzenie…',
|
||||
'auth.quickConnectCancel' => 'Anuluj',
|
||||
'auth.quickConnectExpired' => 'Quick Connect wygasł. Spróbuj ponownie.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Anuluj',
|
||||
'common.save' => 'Zapisz',
|
||||
'common.close' => 'Zamknij',
|
||||
@@ -2239,8 +2252,6 @@ extension on TranslationsPl {
|
||||
'settings.libraryDensity' => 'Gęstość biblioteki',
|
||||
'settings.compact' => 'Kompaktowy',
|
||||
'settings.comfortable' => 'Wygodny',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Tryb widoku',
|
||||
'settings.gridView' => 'Siatka',
|
||||
'settings.listView' => 'Lista',
|
||||
@@ -2310,7 +2321,7 @@ extension on TranslationsPl {
|
||||
'settings.watchTogetherRelay' => 'Relay Oglądaj Razem',
|
||||
'settings.watchTogetherRelayDescription' => 'Ustaw własny relay. Wszyscy muszą używać tego samego serwera.',
|
||||
'settings.watchTogetherRelayHint' => 'https://moj-relay.przyklad.pl',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Raportowanie błędów',
|
||||
'settings.crashReportingDescription' => 'Wysyłaj raporty o błędach, aby pomóc ulepszyć aplikację',
|
||||
'settings.debugLogging' => 'Logowanie debugowania',
|
||||
@@ -2327,12 +2338,10 @@ extension on TranslationsPl {
|
||||
'settings.exportSettings' => 'Eksportuj ustawienia',
|
||||
'settings.exportSettingsDescription' => 'Zapisz swoje preferencje do pliku',
|
||||
'settings.exportSettingsSuccess' => 'Ustawienia wyeksportowane',
|
||||
'settings.exportSettingsFailed' => 'Nie można wyeksportować ustawień',
|
||||
'settings.importSettings' => 'Importuj ustawienia',
|
||||
'settings.importSettingsDescription' => 'Przywróć preferencje z pliku',
|
||||
'settings.importSettingsConfirm' => 'Bieżące ustawienia zostaną zastąpione. Kontynuować?',
|
||||
'settings.importSettingsSuccess' => 'Ustawienia zaimportowane',
|
||||
'settings.importSettingsFailed' => 'Nie można zaimportować ustawień',
|
||||
'settings.importSettingsInvalidFile' => 'Ten plik nie jest prawidłowym eksportem Plezy',
|
||||
'settings.importSettingsNoUser' => 'Zaloguj się przed importem ustawień',
|
||||
'settings.shortcutsReset' => 'Skróty przywrócone do domyślnych',
|
||||
@@ -2347,6 +2356,7 @@ extension on TranslationsPl {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Czas musi być między ${min} a ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Skrót jest już przypisany do ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Skrót zaktualizowany dla ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Automatyczne pomijanie',
|
||||
'settings.autoSkipIntro' => 'Automatyczne pomijanie intro',
|
||||
'settings.autoSkipIntroDescription' => 'Automatycznie pomijaj znaczniki intro po kilku sekundach',
|
||||
@@ -2372,7 +2382,7 @@ extension on TranslationsPl {
|
||||
'settings.downloadLocationChanged' => 'Lokalizacja pobierania zmieniona',
|
||||
'settings.downloadLocationReset' => 'Lokalizacja pobierania przywrócona do domyślnej',
|
||||
'settings.downloadLocationInvalid' => 'Wybrany folder nie jest zapisywalny',
|
||||
'settings.downloadLocationSelectError' => 'Nie udało się wybrać folderu',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Wybór folderu nie jest dostępny na tym urządzeniu',
|
||||
'settings.downloadOnWifiOnly' => 'Pobieraj tylko przez WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Blokuj pobieranie na danych komórkowych',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Automatycznie usuwaj obejrzane pobrania',
|
||||
@@ -2560,10 +2570,15 @@ extension on TranslationsPl {
|
||||
'accessibility.hexColor' => 'Kolor szesnastkowy',
|
||||
'accessibility.expandText' => 'Rozwiń tekst',
|
||||
'accessibility.collapseText' => 'Zwiń tekst',
|
||||
'accessibility.alphabetNavigation' => 'Nawigacja alfabetyczna',
|
||||
'accessibility.alphabetScrollHint' => 'Przesuń w górę lub w dół, aby przejść o literę',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Wiersz ${row} z ${rowCount}, kolumna ${column} z ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Wiersz ${row} z ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Odtwarzanie losowe',
|
||||
'tooltips.playTrailer' => 'Odtwórz zwiastun',
|
||||
'tooltips.markAsWatched' => 'Oznacz jako obejrzane',
|
||||
'tooltips.markAsUnwatched' => 'Oznacz jako nieobejrzane',
|
||||
'audioTracks.track' => ({required Object n}) => 'Ścieżka audio ${n}',
|
||||
'videoControls.audioLabel' => 'Audio',
|
||||
'videoControls.subtitlesLabel' => 'Napisy',
|
||||
'videoControls.resetToZero' => 'Zresetuj do 0ms',
|
||||
@@ -2650,20 +2665,20 @@ extension on TranslationsPl {
|
||||
'messages.markedAsUnwatched' => 'Oznaczono jako nieobejrzane',
|
||||
'messages.markedAsWatchedOffline' => 'Oznaczono jako obejrzane (zsynchronizuje się po połączeniu)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Oznaczono jako nieobejrzane (zsynchronizuje się po połączeniu)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatycznie usunięto: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))(n, one: 'Automatycznie usunięto ${n} obejrzane pobranie', few: 'Automatycznie usunięto ${n} obejrzane pobrania', many: 'Automatycznie usunięto ${n} obejrzanych pobrań', other: 'Automatycznie usunięto ${n} obejrzanego pobrania', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatycznie usunięto: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))(n, one: 'Automatycznie usunięto ${n} obejrzane pobranie', few: 'Automatycznie usunięto ${n} obejrzane pobrania', many: 'Automatycznie usunięto ${n} obejrzanych pobrań', other: 'Automatycznie usunięto ${n} obejrzanego pobrania', ),
|
||||
'messages.removedFromContinueWatching' => 'Usunięto z kontynuowania oglądania',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Błąd: ${error}',
|
||||
'messages.streamInterrupted' => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.',
|
||||
'messages.liveStreamInterrupted' => 'Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.',
|
||||
'messages.fileInfoNotAvailable' => 'Informacje o pliku niedostępne',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Błąd ładowania serialu',
|
||||
'messages.musicNotSupported' => 'Odtwarzanie muzyki nie jest jeszcze obsługiwane',
|
||||
@@ -2778,6 +2793,7 @@ extension on TranslationsPl {
|
||||
'profiles.borrowExplain' => 'Pożycz połączenie z innego profilu. Profile chronione PIN wymagają PIN-u.',
|
||||
'profiles.borrowEmpty' => 'Nic do pożyczenia.',
|
||||
'profiles.borrowEmptySubtitle' => 'Najpierw połącz Plex lub Jellyfin z innym profilem.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Od ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Połączenie pożyczone.',
|
||||
'profiles.borrowFailed' => 'Nie udało się pożyczyć połączenia.',
|
||||
@@ -3003,7 +3019,7 @@ extension on TranslationsPl {
|
||||
'liveTv.favorites' => 'Ulubione',
|
||||
'liveTv.reorderFavorites' => 'Zmień kolejność ulubionych',
|
||||
'liveTv.favoritesLoadFailed' => 'Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Dołącz do trwającej sesji',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)',
|
||||
'liveTv.watchLive' => 'Oglądaj na żywo',
|
||||
@@ -3163,11 +3179,11 @@ extension on TranslationsPl {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} opuścił',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} wstrzymał',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} wznowił',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} przewinął',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} buforuje',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} używa starszej wersji aplikacji — synchronizacja jest niedostępna',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Wznawianie bez ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Oczekiwanie na załadowanie u innych...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Oczekiwanie na ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetPt rateSheet = _TranslationsRateSheetPt._(_root);
|
||||
@override late final _TranslationsAccessibilityPt accessibility = _TranslationsAccessibilityPt._(_root);
|
||||
@override late final _TranslationsTooltipsPt tooltips = _TranslationsTooltipsPt._(_root);
|
||||
@override late final _TranslationsAudioTracksPt audioTracks = _TranslationsAudioTracksPt._(_root);
|
||||
@override late final _TranslationsVideoControlsPt videoControls = _TranslationsVideoControlsPt._(_root);
|
||||
@override late final _TranslationsMessagesPt messages = _TranslationsMessagesPt._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingPt subtitlingStyling = _TranslationsSubtitlingStylingPt._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthPt extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'A aguardar aprovação…';
|
||||
@override String get quickConnectCancel => 'Cancelar';
|
||||
@override String get quickConnectExpired => 'Quick Connect expirou. Tente novamente.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Densidade da Biblioteca';
|
||||
@override String get compact => 'Compacto';
|
||||
@override String get comfortable => 'Confortável';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Modo de Visualização';
|
||||
@override String get gridView => 'Grade';
|
||||
@override String get listView => 'Lista';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relay do Assistir Juntos';
|
||||
@override String get watchTogetherRelayDescription => 'Defina um relay personalizado. Todos devem usar o mesmo servidor.';
|
||||
@override String get watchTogetherRelayHint => 'https://meu-relay.exemplo.com.br';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Relatório de Erros';
|
||||
@override String get crashReportingDescription => 'Enviar relatórios de erros para ajudar a melhorar o app';
|
||||
@override String get debugLogging => 'Log de Depuração';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Exportar Configurações';
|
||||
@override String get exportSettingsDescription => 'Salve suas preferências em um arquivo';
|
||||
@override String get exportSettingsSuccess => 'Configurações exportadas';
|
||||
@override String get exportSettingsFailed => 'Não foi possível exportar as configurações';
|
||||
@override String get importSettings => 'Importar Configurações';
|
||||
@override String get importSettingsDescription => 'Restaurar preferências a partir de um arquivo';
|
||||
@override String get importSettingsConfirm => 'Isso substituirá suas configurações atuais. Continuar?';
|
||||
@override String get importSettingsSuccess => 'Configurações importadas';
|
||||
@override String get importSettingsFailed => 'Não foi possível importar as configurações';
|
||||
@override String get importSettingsInvalidFile => 'Este arquivo não é uma exportação válida do Plezy';
|
||||
@override String get importSettingsNoUser => 'Entre na conta antes de importar as configurações';
|
||||
@override String get shortcutsReset => 'Atalhos redefinidos para o padrão';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'A duração deve ser entre ${min} e ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Atalho já atribuído a ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Atalho atualizado para ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Pular Automaticamente';
|
||||
@override String get autoSkipIntro => 'Pular Intro Automaticamente';
|
||||
@override String get autoSkipIntroDescription => 'Pular marcadores de intro automaticamente após alguns segundos';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Local de download alterado';
|
||||
@override String get downloadLocationReset => 'Local de download redefinido para padrão';
|
||||
@override String get downloadLocationInvalid => 'A pasta selecionada não permite gravação';
|
||||
@override String get downloadLocationSelectError => 'Falha ao selecionar pasta';
|
||||
@override String get downloadLocationPickerUnavailable => 'A seleção de pasta não está disponível neste dispositivo';
|
||||
@override String get downloadOnWifiOnly => 'Baixar apenas no WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Impedir downloads quando em dados móveis';
|
||||
@override String get autoRemoveWatchedDownloads => 'Remover downloads assistidos automaticamente';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityPt extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Cor hexadecimal';
|
||||
@override String get expandText => 'Expandir texto';
|
||||
@override String get collapseText => 'Recolher texto';
|
||||
@override String get alphabetNavigation => 'Navegação alfabética';
|
||||
@override String get alphabetScrollHint => 'Deslize para cima ou para baixo para avançar por letra';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Linha ${row} de ${rowCount}, coluna ${column} de ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Linha ${row} de ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsPt extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Marcar como não assistido';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksPt extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksPt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsPt _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Faixa de áudio ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsPt extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsPt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesPt extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.';
|
||||
@override String get liveStreamInterrupted => 'A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.';
|
||||
@override String get fileInfoNotAvailable => 'Informações do arquivo não disponíveis';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Erro ao carregar info do arquivo: ${error}';
|
||||
@override String get errorLoadingSeries => 'Erro ao carregar série';
|
||||
@override String get musicNotSupported => 'Reprodução de música ainda não é suportada';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesPt extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Use a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.';
|
||||
@override String get borrowEmpty => 'Nada para emprestar ainda.';
|
||||
@override String get borrowEmptySubtitle => 'Conecte Plex ou Jellyfin a outro perfil primeiro.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'De ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Conexão tomada emprestada.';
|
||||
@override String get borrowFailed => 'Não foi possível tomar a conexão emprestada.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvPt extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoritos';
|
||||
@override String get reorderFavorites => 'Reordenar favoritos';
|
||||
@override String get favoritesLoadFailed => 'Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Entrar na sessão em andamento';
|
||||
@override String watchFromStart({required Object minutes}) => 'Assistir do início (${minutes} min atrás)';
|
||||
@override String get watchLive => 'Assistir ao vivo';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsPt {
|
||||
'auth.quickConnectWaiting' => 'A aguardar aprovação…',
|
||||
'auth.quickConnectCancel' => 'Cancelar',
|
||||
'auth.quickConnectExpired' => 'Quick Connect expirou. Tente novamente.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Cancelar',
|
||||
'common.save' => 'Salvar',
|
||||
'common.close' => 'Fechar',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsPt {
|
||||
'settings.libraryDensity' => 'Densidade da Biblioteca',
|
||||
'settings.compact' => 'Compacto',
|
||||
'settings.comfortable' => 'Confortável',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Modo de Visualização',
|
||||
'settings.gridView' => 'Grade',
|
||||
'settings.listView' => 'Lista',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsPt {
|
||||
'settings.watchTogetherRelay' => 'Relay do Assistir Juntos',
|
||||
'settings.watchTogetherRelayDescription' => 'Defina um relay personalizado. Todos devem usar o mesmo servidor.',
|
||||
'settings.watchTogetherRelayHint' => 'https://meu-relay.exemplo.com.br',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Relatório de Erros',
|
||||
'settings.crashReportingDescription' => 'Enviar relatórios de erros para ajudar a melhorar o app',
|
||||
'settings.debugLogging' => 'Log de Depuração',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsPt {
|
||||
'settings.exportSettings' => 'Exportar Configurações',
|
||||
'settings.exportSettingsDescription' => 'Salve suas preferências em um arquivo',
|
||||
'settings.exportSettingsSuccess' => 'Configurações exportadas',
|
||||
'settings.exportSettingsFailed' => 'Não foi possível exportar as configurações',
|
||||
'settings.importSettings' => 'Importar Configurações',
|
||||
'settings.importSettingsDescription' => 'Restaurar preferências a partir de um arquivo',
|
||||
'settings.importSettingsConfirm' => 'Isso substituirá suas configurações atuais. Continuar?',
|
||||
'settings.importSettingsSuccess' => 'Configurações importadas',
|
||||
'settings.importSettingsFailed' => 'Não foi possível importar as configurações',
|
||||
'settings.importSettingsInvalidFile' => 'Este arquivo não é uma exportação válida do Plezy',
|
||||
'settings.importSettingsNoUser' => 'Entre na conta antes de importar as configurações',
|
||||
'settings.shortcutsReset' => 'Atalhos redefinidos para o padrão',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsPt {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'A duração deve ser entre ${min} e ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Atalho já atribuído a ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Atalho atualizado para ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Pular Automaticamente',
|
||||
'settings.autoSkipIntro' => 'Pular Intro Automaticamente',
|
||||
'settings.autoSkipIntroDescription' => 'Pular marcadores de intro automaticamente após alguns segundos',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsPt {
|
||||
'settings.downloadLocationChanged' => 'Local de download alterado',
|
||||
'settings.downloadLocationReset' => 'Local de download redefinido para padrão',
|
||||
'settings.downloadLocationInvalid' => 'A pasta selecionada não permite gravação',
|
||||
'settings.downloadLocationSelectError' => 'Falha ao selecionar pasta',
|
||||
'settings.downloadLocationPickerUnavailable' => 'A seleção de pasta não está disponível neste dispositivo',
|
||||
'settings.downloadOnWifiOnly' => 'Baixar apenas no WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Impedir downloads quando em dados móveis',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Remover downloads assistidos automaticamente',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsPt {
|
||||
'accessibility.hexColor' => 'Cor hexadecimal',
|
||||
'accessibility.expandText' => 'Expandir texto',
|
||||
'accessibility.collapseText' => 'Recolher texto',
|
||||
'accessibility.alphabetNavigation' => 'Navegação alfabética',
|
||||
'accessibility.alphabetScrollHint' => 'Deslize para cima ou para baixo para avançar por letra',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Linha ${row} de ${rowCount}, coluna ${column} de ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Linha ${row} de ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Reprodução aleatória',
|
||||
'tooltips.playTrailer' => 'Reproduzir trailer',
|
||||
'tooltips.markAsWatched' => 'Marcar como assistido',
|
||||
'tooltips.markAsUnwatched' => 'Marcar como não assistido',
|
||||
'audioTracks.track' => ({required Object n}) => 'Faixa de áudio ${n}',
|
||||
'videoControls.audioLabel' => 'Áudio',
|
||||
'videoControls.subtitlesLabel' => 'Legendas',
|
||||
'videoControls.resetToZero' => 'Redefinir para 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsPt {
|
||||
'messages.markedAsUnwatched' => 'Marcado como não assistido',
|
||||
'messages.markedAsWatchedOffline' => 'Marcado como assistido (será sincronizado quando online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Marcado como não assistido (será sincronizado quando online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Removido automaticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pt'))(n, one: 'Removido automaticamente ${n} download assistido', other: 'Removidos automaticamente ${n} downloads assistidos', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Removido automaticamente: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pt'))(n, one: 'Removido automaticamente ${n} download assistido', other: 'Removidos automaticamente ${n} downloads assistidos', ),
|
||||
'messages.removedFromContinueWatching' => 'Removido de Continuar Assistindo',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Erro: ${error}',
|
||||
'messages.streamInterrupted' => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.',
|
||||
'messages.liveStreamInterrupted' => 'A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.',
|
||||
'messages.fileInfoNotAvailable' => 'Informações do arquivo não disponíveis',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erro ao carregar info do arquivo: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Erro ao carregar série',
|
||||
'messages.musicNotSupported' => 'Reprodução de música ainda não é suportada',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsPt {
|
||||
'profiles.borrowExplain' => 'Use a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.',
|
||||
'profiles.borrowEmpty' => 'Nada para emprestar ainda.',
|
||||
'profiles.borrowEmptySubtitle' => 'Conecte Plex ou Jellyfin a outro perfil primeiro.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'De ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Conexão tomada emprestada.',
|
||||
'profiles.borrowFailed' => 'Não foi possível tomar a conexão emprestada.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsPt {
|
||||
'liveTv.favorites' => 'Favoritos',
|
||||
'liveTv.reorderFavorites' => 'Reordenar favoritos',
|
||||
'liveTv.favoritesLoadFailed' => 'Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Entrar na sessão em andamento',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Assistir do início (${minutes} min atrás)',
|
||||
'liveTv.watchLive' => 'Assistir ao vivo',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsPt {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} saiu',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} pausou',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} retomou',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} avançou',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} está carregando',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} está em uma versão mais antiga do aplicativo — sincronização indisponível',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Retomando sem ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Aguardando outros carregarem...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Aguardando ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsRu extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetRu rateSheet = _TranslationsRateSheetRu._(_root);
|
||||
@override late final _TranslationsAccessibilityRu accessibility = _TranslationsAccessibilityRu._(_root);
|
||||
@override late final _TranslationsTooltipsRu tooltips = _TranslationsTooltipsRu._(_root);
|
||||
@override late final _TranslationsAudioTracksRu audioTracks = _TranslationsAudioTracksRu._(_root);
|
||||
@override late final _TranslationsVideoControlsRu videoControls = _TranslationsVideoControlsRu._(_root);
|
||||
@override late final _TranslationsMessagesRu messages = _TranslationsMessagesRu._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingRu subtitlingStyling = _TranslationsSubtitlingStylingRu._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthRu extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Ожидание подтверждения…';
|
||||
@override String get quickConnectCancel => 'Отмена';
|
||||
@override String get quickConnectExpired => 'Срок Quick Connect истек. Попробуйте снова.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Плотность библиотеки';
|
||||
@override String get compact => 'Компактный';
|
||||
@override String get comfortable => 'Комфортный';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Режим просмотра';
|
||||
@override String get gridView => 'Сетка';
|
||||
@override String get listView => 'Список';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Relay совместного просмотра';
|
||||
@override String get watchTogetherRelayDescription => 'Задайте свой relay. Все должны использовать один сервер.';
|
||||
@override String get watchTogetherRelayHint => 'https://my-relay.example.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Отчёты об ошибках';
|
||||
@override String get crashReportingDescription => 'Отправлять отчёты об ошибках для улучшения приложения';
|
||||
@override String get debugLogging => 'Журнал отладки';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Экспорт настроек';
|
||||
@override String get exportSettingsDescription => 'Сохранить настройки в файл';
|
||||
@override String get exportSettingsSuccess => 'Настройки экспортированы';
|
||||
@override String get exportSettingsFailed => 'Не удалось экспортировать настройки';
|
||||
@override String get importSettings => 'Импорт настроек';
|
||||
@override String get importSettingsDescription => 'Восстановить настройки из файла';
|
||||
@override String get importSettingsConfirm => 'Это заменит ваши текущие настройки. Продолжить?';
|
||||
@override String get importSettingsSuccess => 'Настройки импортированы';
|
||||
@override String get importSettingsFailed => 'Не удалось импортировать настройки';
|
||||
@override String get importSettingsInvalidFile => 'Этот файл не является действительным экспортом настроек Plezy';
|
||||
@override String get importSettingsNoUser => 'Войдите в систему перед импортом настроек';
|
||||
@override String get shortcutsReset => 'Горячие клавиши сброшены по умолчанию';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Длительность должна быть от ${min} до ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Клавиша уже назначена для ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Клавиша обновлена для ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Автопропуск';
|
||||
@override String get autoSkipIntro => 'Автопропуск вступления';
|
||||
@override String get autoSkipIntroDescription => 'Автоматически пропускать маркеры вступления через несколько секунд';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Место загрузки изменено';
|
||||
@override String get downloadLocationReset => 'Место загрузки сброшено по умолчанию';
|
||||
@override String get downloadLocationInvalid => 'Выбранная папка недоступна для записи';
|
||||
@override String get downloadLocationSelectError => 'Не удалось выбрать папку';
|
||||
@override String get downloadLocationPickerUnavailable => 'Выбор папки недоступен на этом устройстве';
|
||||
@override String get downloadOnWifiOnly => 'Загружать только по WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Запретить загрузку по мобильным данным';
|
||||
@override String get autoRemoveWatchedDownloads => 'Автоудаление просмотренных загрузок';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityRu extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Шестнадцатеричный цвет';
|
||||
@override String get expandText => 'Развернуть текст';
|
||||
@override String get collapseText => 'Свернуть текст';
|
||||
@override String get alphabetNavigation => 'Навигация по алфавиту';
|
||||
@override String get alphabetScrollHint => 'Проведите вверх или вниз для перехода по буквам';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Строка ${row} из ${rowCount}, столбец ${column} из ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Строка ${row} из ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsRu extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Отметить как непросмотренное';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksRu extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksRu._(TranslationsRu root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsRu _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Аудиодорожка ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsRu extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsRu._(TranslationsRu root) : this._root = root, super.internal(root);
|
||||
@@ -714,11 +726,11 @@ class _TranslationsMessagesRu extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.';
|
||||
@override String get liveStreamInterrupted => 'Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.';
|
||||
@override String get fileInfoNotAvailable => 'Информация о файле недоступна';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Ошибка загрузки информации о файле: ${error}';
|
||||
@override String get errorLoadingSeries => 'Ошибка загрузки сериала';
|
||||
@override String get musicNotSupported => 'Воспроизведение музыки пока не поддерживается';
|
||||
@@ -869,6 +881,7 @@ class _TranslationsProfilesRu extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Заимствуйте подключение другого профиля. Для профилей с PIN нужен PIN.';
|
||||
@override String get borrowEmpty => 'Пока нечего заимствовать.';
|
||||
@override String get borrowEmptySubtitle => 'Сначала подключите Plex или Jellyfin к другому профилю.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Из ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Подключение заимствовано.';
|
||||
@override String get borrowFailed => 'Не удалось заимствовать подключение.';
|
||||
@@ -1154,7 +1167,7 @@ class _TranslationsLiveTvRu extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Избранное';
|
||||
@override String get reorderFavorites => 'Изменить порядок избранного';
|
||||
@override String get favoritesLoadFailed => 'Не удалось загрузить избранное. Проверьте подключение и повторите попытку.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Присоединиться к текущему сеансу';
|
||||
@override String watchFromStart({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)';
|
||||
@override String get watchLive => 'Смотреть в прямом эфире';
|
||||
@@ -2155,7 +2168,7 @@ extension on TranslationsRu {
|
||||
'auth.quickConnectWaiting' => 'Ожидание подтверждения…',
|
||||
'auth.quickConnectCancel' => 'Отмена',
|
||||
'auth.quickConnectExpired' => 'Срок Quick Connect истек. Попробуйте снова.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Отмена',
|
||||
'common.save' => 'Сохранить',
|
||||
'common.close' => 'Закрыть',
|
||||
@@ -2239,8 +2252,6 @@ extension on TranslationsRu {
|
||||
'settings.libraryDensity' => 'Плотность библиотеки',
|
||||
'settings.compact' => 'Компактный',
|
||||
'settings.comfortable' => 'Комфортный',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Режим просмотра',
|
||||
'settings.gridView' => 'Сетка',
|
||||
'settings.listView' => 'Список',
|
||||
@@ -2310,7 +2321,7 @@ extension on TranslationsRu {
|
||||
'settings.watchTogetherRelay' => 'Relay совместного просмотра',
|
||||
'settings.watchTogetherRelayDescription' => 'Задайте свой relay. Все должны использовать один сервер.',
|
||||
'settings.watchTogetherRelayHint' => 'https://my-relay.example.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Отчёты об ошибках',
|
||||
'settings.crashReportingDescription' => 'Отправлять отчёты об ошибках для улучшения приложения',
|
||||
'settings.debugLogging' => 'Журнал отладки',
|
||||
@@ -2327,12 +2338,10 @@ extension on TranslationsRu {
|
||||
'settings.exportSettings' => 'Экспорт настроек',
|
||||
'settings.exportSettingsDescription' => 'Сохранить настройки в файл',
|
||||
'settings.exportSettingsSuccess' => 'Настройки экспортированы',
|
||||
'settings.exportSettingsFailed' => 'Не удалось экспортировать настройки',
|
||||
'settings.importSettings' => 'Импорт настроек',
|
||||
'settings.importSettingsDescription' => 'Восстановить настройки из файла',
|
||||
'settings.importSettingsConfirm' => 'Это заменит ваши текущие настройки. Продолжить?',
|
||||
'settings.importSettingsSuccess' => 'Настройки импортированы',
|
||||
'settings.importSettingsFailed' => 'Не удалось импортировать настройки',
|
||||
'settings.importSettingsInvalidFile' => 'Этот файл не является действительным экспортом настроек Plezy',
|
||||
'settings.importSettingsNoUser' => 'Войдите в систему перед импортом настроек',
|
||||
'settings.shortcutsReset' => 'Горячие клавиши сброшены по умолчанию',
|
||||
@@ -2347,6 +2356,7 @@ extension on TranslationsRu {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Длительность должна быть от ${min} до ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Клавиша уже назначена для ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Клавиша обновлена для ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Автопропуск',
|
||||
'settings.autoSkipIntro' => 'Автопропуск вступления',
|
||||
'settings.autoSkipIntroDescription' => 'Автоматически пропускать маркеры вступления через несколько секунд',
|
||||
@@ -2372,7 +2382,7 @@ extension on TranslationsRu {
|
||||
'settings.downloadLocationChanged' => 'Место загрузки изменено',
|
||||
'settings.downloadLocationReset' => 'Место загрузки сброшено по умолчанию',
|
||||
'settings.downloadLocationInvalid' => 'Выбранная папка недоступна для записи',
|
||||
'settings.downloadLocationSelectError' => 'Не удалось выбрать папку',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Выбор папки недоступен на этом устройстве',
|
||||
'settings.downloadOnWifiOnly' => 'Загружать только по WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Запретить загрузку по мобильным данным',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Автоудаление просмотренных загрузок',
|
||||
@@ -2560,10 +2570,15 @@ extension on TranslationsRu {
|
||||
'accessibility.hexColor' => 'Шестнадцатеричный цвет',
|
||||
'accessibility.expandText' => 'Развернуть текст',
|
||||
'accessibility.collapseText' => 'Свернуть текст',
|
||||
'accessibility.alphabetNavigation' => 'Навигация по алфавиту',
|
||||
'accessibility.alphabetScrollHint' => 'Проведите вверх или вниз для перехода по буквам',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Строка ${row} из ${rowCount}, столбец ${column} из ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Строка ${row} из ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Случайное воспроизведение',
|
||||
'tooltips.playTrailer' => 'Воспроизвести трейлер',
|
||||
'tooltips.markAsWatched' => 'Отметить как просмотренное',
|
||||
'tooltips.markAsUnwatched' => 'Отметить как непросмотренное',
|
||||
'audioTracks.track' => ({required Object n}) => 'Аудиодорожка ${n}',
|
||||
'videoControls.audioLabel' => 'Аудио',
|
||||
'videoControls.subtitlesLabel' => 'Субтитры',
|
||||
'videoControls.resetToZero' => 'Сбросить до 0мс',
|
||||
@@ -2650,20 +2665,20 @@ extension on TranslationsRu {
|
||||
'messages.markedAsUnwatched' => 'Отмечено как непросмотренное',
|
||||
'messages.markedAsWatchedOffline' => 'Отмечено как просмотренное (синхронизируется при подключении)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Отмечено как непросмотренное (синхронизируется при подключении)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Автоудалено: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, one: 'Автоматически удалена ${n} просмотренная загрузка', few: 'Автоматически удалены ${n} просмотренные загрузки', many: 'Автоматически удалено ${n} просмотренных загрузок', other: 'Автоматически удалено ${n} просмотренной загрузки', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Автоудалено: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, one: 'Автоматически удалена ${n} просмотренная загрузка', few: 'Автоматически удалены ${n} просмотренные загрузки', many: 'Автоматически удалено ${n} просмотренных загрузок', other: 'Автоматически удалено ${n} просмотренной загрузки', ),
|
||||
'messages.removedFromContinueWatching' => 'Удалено из «Продолжить просмотр»',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Ошибка: ${error}',
|
||||
'messages.streamInterrupted' => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.',
|
||||
'messages.liveStreamInterrupted' => 'Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.',
|
||||
'messages.fileInfoNotAvailable' => 'Информация о файле недоступна',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Ошибка загрузки информации о файле: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Ошибка загрузки сериала',
|
||||
'messages.musicNotSupported' => 'Воспроизведение музыки пока не поддерживается',
|
||||
@@ -2778,6 +2793,7 @@ extension on TranslationsRu {
|
||||
'profiles.borrowExplain' => 'Заимствуйте подключение другого профиля. Для профилей с PIN нужен PIN.',
|
||||
'profiles.borrowEmpty' => 'Пока нечего заимствовать.',
|
||||
'profiles.borrowEmptySubtitle' => 'Сначала подключите Plex или Jellyfin к другому профилю.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Из ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Подключение заимствовано.',
|
||||
'profiles.borrowFailed' => 'Не удалось заимствовать подключение.',
|
||||
@@ -3003,7 +3019,7 @@ extension on TranslationsRu {
|
||||
'liveTv.favorites' => 'Избранное',
|
||||
'liveTv.reorderFavorites' => 'Изменить порядок избранного',
|
||||
'liveTv.favoritesLoadFailed' => 'Не удалось загрузить избранное. Проверьте подключение и повторите попытку.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Присоединиться к текущему сеансу',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)',
|
||||
'liveTv.watchLive' => 'Смотреть в прямом эфире',
|
||||
@@ -3163,11 +3179,11 @@ extension on TranslationsRu {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} вышел',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} поставил на паузу',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} возобновил',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} перемотал',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} буферизует',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} использует старую версию приложения — синхронизация недоступна',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Возобновление без ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Ожидание загрузки у других...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Ожидание ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsSv extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetSv rateSheet = _TranslationsRateSheetSv._(_root);
|
||||
@override late final _TranslationsAccessibilitySv accessibility = _TranslationsAccessibilitySv._(_root);
|
||||
@override late final _TranslationsTooltipsSv tooltips = _TranslationsTooltipsSv._(_root);
|
||||
@override late final _TranslationsAudioTracksSv audioTracks = _TranslationsAudioTracksSv._(_root);
|
||||
@override late final _TranslationsVideoControlsSv videoControls = _TranslationsVideoControlsSv._(_root);
|
||||
@override late final _TranslationsMessagesSv messages = _TranslationsMessagesSv._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingSv subtitlingStyling = _TranslationsSubtitlingStylingSv._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthSv extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => 'Väntar på godkännande…';
|
||||
@override String get quickConnectCancel => 'Avbryt';
|
||||
@override String get quickConnectExpired => 'Quick Connect har gått ut. Försök igen.';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => 'Biblioteksdensitet';
|
||||
@override String get compact => 'Kompakt';
|
||||
@override String get comfortable => 'Bekväm';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => 'Visningsläge';
|
||||
@override String get gridView => 'Rutnät';
|
||||
@override String get listView => 'Lista';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => 'Titta Tillsammans-relay';
|
||||
@override String get watchTogetherRelayDescription => 'Ange en anpassad relay. Alla måste använda samma server.';
|
||||
@override String get watchTogetherRelayHint => 'https://min-relay.exempel.se';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => 'Kraschrapportering';
|
||||
@override String get crashReportingDescription => 'Skicka kraschrapporter för att förbättra appen';
|
||||
@override String get debugLogging => 'Felsökningsloggning';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => 'Exportera inställningar';
|
||||
@override String get exportSettingsDescription => 'Spara dina inställningar till en fil';
|
||||
@override String get exportSettingsSuccess => 'Inställningar exporterade';
|
||||
@override String get exportSettingsFailed => 'Kunde inte exportera inställningar';
|
||||
@override String get importSettings => 'Importera inställningar';
|
||||
@override String get importSettingsDescription => 'Återställ inställningar från en fil';
|
||||
@override String get importSettingsConfirm => 'Detta ersätter dina nuvarande inställningar. Fortsätta?';
|
||||
@override String get importSettingsSuccess => 'Inställningar importerade';
|
||||
@override String get importSettingsFailed => 'Kunde inte importera inställningar';
|
||||
@override String get importSettingsInvalidFile => 'Denna fil är inte en giltig Plezy-export';
|
||||
@override String get importSettingsNoUser => 'Logga in innan du importerar inställningar';
|
||||
@override String get shortcutsReset => 'Genvägar återställda till standard';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Tiden måste vara mellan ${min} och ${max} ${unit}';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => 'Genväg redan tilldelad ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => 'Genväg uppdaterad för ${action}';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => 'Auto Hoppa Över';
|
||||
@override String get autoSkipIntro => 'Hoppa Över Intro Automatiskt';
|
||||
@override String get autoSkipIntroDescription => 'Hoppa automatiskt över intro-markörer efter några sekunder';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => 'Nedladdningsplats ändrad';
|
||||
@override String get downloadLocationReset => 'Nedladdningsplats återställd till standard';
|
||||
@override String get downloadLocationInvalid => 'Vald mapp är inte skrivbar';
|
||||
@override String get downloadLocationSelectError => 'Kunde inte välja mapp';
|
||||
@override String get downloadLocationPickerUnavailable => 'Mappval är inte tillgängligt på den här enheten';
|
||||
@override String get downloadOnWifiOnly => 'Ladda ner endast på WiFi';
|
||||
@override String get downloadOnWifiOnlyDescription => 'Förhindra nedladdningar vid användning av mobildata';
|
||||
@override String get autoRemoveWatchedDownloads => 'Ta bort sedda nedladdningar automatiskt';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilitySv extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => 'Hexfärg';
|
||||
@override String get expandText => 'Expandera text';
|
||||
@override String get collapseText => 'Fäll ihop text';
|
||||
@override String get alphabetNavigation => 'Alfabetisk navigering';
|
||||
@override String get alphabetScrollHint => 'Svep upp eller ned för att flytta en bokstav';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rad ${row} av ${rowCount}, kolumn ${column} av ${columnCount}';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => 'Rad ${row} av ${rowCount}';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsSv extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => 'Markera som osedd';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksSv extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksSv._(TranslationsSv root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsSv _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => 'Ljudspår ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsSv extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsSv._(TranslationsSv root) : this._root = root, super.internal(root);
|
||||
@@ -712,11 +724,11 @@ class _TranslationsMessagesSv extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.';
|
||||
@override String get liveStreamInterrupted => 'Livestreamen avbröts. Tryck på play för att försöka igen.';
|
||||
@override String get fileInfoNotAvailable => 'Filinformation inte tillgänglig';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => 'Fel vid laddning av filinformation: ${error}';
|
||||
@override String get errorLoadingSeries => 'Fel vid laddning av serie';
|
||||
@override String get musicNotSupported => 'Musikuppspelning stöds inte ännu';
|
||||
@@ -867,6 +879,7 @@ class _TranslationsProfilesSv extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => 'Låna en annan profils anslutning. PIN-skyddade profiler kräver en PIN.';
|
||||
@override String get borrowEmpty => 'Inget att låna ännu.';
|
||||
@override String get borrowEmptySubtitle => 'Anslut Plex eller Jellyfin till en annan profil först.';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => 'Från ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => 'Anslutning lånad.';
|
||||
@override String get borrowFailed => 'Kunde inte låna anslutningen.';
|
||||
@@ -1150,7 +1163,7 @@ class _TranslationsLiveTvSv extends TranslationsLiveTvEn {
|
||||
@override String get favorites => 'Favoriter';
|
||||
@override String get reorderFavorites => 'Ordna om favoriter';
|
||||
@override String get favoritesLoadFailed => 'Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => 'Gå med i pågående session';
|
||||
@override String watchFromStart({required Object minutes}) => 'Titta från början (${minutes} min sedan)';
|
||||
@override String get watchLive => 'Titta live';
|
||||
@@ -2149,7 +2162,7 @@ extension on TranslationsSv {
|
||||
'auth.quickConnectWaiting' => 'Väntar på godkännande…',
|
||||
'auth.quickConnectCancel' => 'Avbryt',
|
||||
'auth.quickConnectExpired' => 'Quick Connect har gått ut. Försök igen.',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => 'Avbryt',
|
||||
'common.save' => 'Spara',
|
||||
'common.close' => 'Stäng',
|
||||
@@ -2233,8 +2246,6 @@ extension on TranslationsSv {
|
||||
'settings.libraryDensity' => 'Biblioteksdensitet',
|
||||
'settings.compact' => 'Kompakt',
|
||||
'settings.comfortable' => 'Bekväm',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => 'Visningsläge',
|
||||
'settings.gridView' => 'Rutnät',
|
||||
'settings.listView' => 'Lista',
|
||||
@@ -2304,7 +2315,7 @@ extension on TranslationsSv {
|
||||
'settings.watchTogetherRelay' => 'Titta Tillsammans-relay',
|
||||
'settings.watchTogetherRelayDescription' => 'Ange en anpassad relay. Alla måste använda samma server.',
|
||||
'settings.watchTogetherRelayHint' => 'https://min-relay.exempel.se',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => 'Kraschrapportering',
|
||||
'settings.crashReportingDescription' => 'Skicka kraschrapporter för att förbättra appen',
|
||||
'settings.debugLogging' => 'Felsökningsloggning',
|
||||
@@ -2321,12 +2332,10 @@ extension on TranslationsSv {
|
||||
'settings.exportSettings' => 'Exportera inställningar',
|
||||
'settings.exportSettingsDescription' => 'Spara dina inställningar till en fil',
|
||||
'settings.exportSettingsSuccess' => 'Inställningar exporterade',
|
||||
'settings.exportSettingsFailed' => 'Kunde inte exportera inställningar',
|
||||
'settings.importSettings' => 'Importera inställningar',
|
||||
'settings.importSettingsDescription' => 'Återställ inställningar från en fil',
|
||||
'settings.importSettingsConfirm' => 'Detta ersätter dina nuvarande inställningar. Fortsätta?',
|
||||
'settings.importSettingsSuccess' => 'Inställningar importerade',
|
||||
'settings.importSettingsFailed' => 'Kunde inte importera inställningar',
|
||||
'settings.importSettingsInvalidFile' => 'Denna fil är inte en giltig Plezy-export',
|
||||
'settings.importSettingsNoUser' => 'Logga in innan du importerar inställningar',
|
||||
'settings.shortcutsReset' => 'Genvägar återställda till standard',
|
||||
@@ -2341,6 +2350,7 @@ extension on TranslationsSv {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => 'Tiden måste vara mellan ${min} och ${max} ${unit}',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Genväg redan tilldelad ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => 'Genväg uppdaterad för ${action}',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => 'Auto Hoppa Över',
|
||||
'settings.autoSkipIntro' => 'Hoppa Över Intro Automatiskt',
|
||||
'settings.autoSkipIntroDescription' => 'Hoppa automatiskt över intro-markörer efter några sekunder',
|
||||
@@ -2366,7 +2376,7 @@ extension on TranslationsSv {
|
||||
'settings.downloadLocationChanged' => 'Nedladdningsplats ändrad',
|
||||
'settings.downloadLocationReset' => 'Nedladdningsplats återställd till standard',
|
||||
'settings.downloadLocationInvalid' => 'Vald mapp är inte skrivbar',
|
||||
'settings.downloadLocationSelectError' => 'Kunde inte välja mapp',
|
||||
'settings.downloadLocationPickerUnavailable' => 'Mappval är inte tillgängligt på den här enheten',
|
||||
'settings.downloadOnWifiOnly' => 'Ladda ner endast på WiFi',
|
||||
'settings.downloadOnWifiOnlyDescription' => 'Förhindra nedladdningar vid användning av mobildata',
|
||||
'settings.autoRemoveWatchedDownloads' => 'Ta bort sedda nedladdningar automatiskt',
|
||||
@@ -2554,10 +2564,15 @@ extension on TranslationsSv {
|
||||
'accessibility.hexColor' => 'Hexfärg',
|
||||
'accessibility.expandText' => 'Expandera text',
|
||||
'accessibility.collapseText' => 'Fäll ihop text',
|
||||
'accessibility.alphabetNavigation' => 'Alfabetisk navigering',
|
||||
'accessibility.alphabetScrollHint' => 'Svep upp eller ned för att flytta en bokstav',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => 'Rad ${row} av ${rowCount}, kolumn ${column} av ${columnCount}',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => 'Rad ${row} av ${rowCount}',
|
||||
'tooltips.shufflePlay' => 'Blanda uppspelning',
|
||||
'tooltips.playTrailer' => 'Spela trailer',
|
||||
'tooltips.markAsWatched' => 'Markera som sedd',
|
||||
'tooltips.markAsUnwatched' => 'Markera som osedd',
|
||||
'audioTracks.track' => ({required Object n}) => 'Ljudspår ${n}',
|
||||
'videoControls.audioLabel' => 'Ljud',
|
||||
'videoControls.subtitlesLabel' => 'Undertexter',
|
||||
'videoControls.resetToZero' => 'Återställ till 0ms',
|
||||
@@ -2644,20 +2659,20 @@ extension on TranslationsSv {
|
||||
'messages.markedAsUnwatched' => 'Markerad som osedd',
|
||||
'messages.markedAsWatchedOffline' => 'Markerad som sedd (synkroniseras när online)',
|
||||
'messages.markedAsUnwatchedOffline' => 'Markerad som osedd (synkroniseras när online)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatiskt borttagen: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n, one: 'Tog automatiskt bort ${n} sedd nedladdning', other: 'Tog automatiskt bort ${n} sedda nedladdningar', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Automatiskt borttagen: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n, one: 'Tog automatiskt bort ${n} sedd nedladdning', other: 'Tog automatiskt bort ${n} sedda nedladdningar', ),
|
||||
'messages.removedFromContinueWatching' => 'Borttagen från Fortsätt titta',
|
||||
'messages.errorLoading' => ({required Object error}) => 'Fel: ${error}',
|
||||
'messages.streamInterrupted' => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.',
|
||||
'messages.liveStreamInterrupted' => 'Livestreamen avbröts. Tryck på play för att försöka igen.',
|
||||
'messages.fileInfoNotAvailable' => 'Filinformation inte tillgänglig',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fel vid laddning av filinformation: ${error}',
|
||||
'messages.errorLoadingSeries' => 'Fel vid laddning av serie',
|
||||
'messages.musicNotSupported' => 'Musikuppspelning stöds inte ännu',
|
||||
@@ -2772,6 +2787,7 @@ extension on TranslationsSv {
|
||||
'profiles.borrowExplain' => 'Låna en annan profils anslutning. PIN-skyddade profiler kräver en PIN.',
|
||||
'profiles.borrowEmpty' => 'Inget att låna ännu.',
|
||||
'profiles.borrowEmptySubtitle' => 'Anslut Plex eller Jellyfin till en annan profil först.',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => 'Från ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => 'Anslutning lånad.',
|
||||
'profiles.borrowFailed' => 'Kunde inte låna anslutningen.',
|
||||
@@ -2997,7 +3013,7 @@ extension on TranslationsSv {
|
||||
'liveTv.favorites' => 'Favoriter',
|
||||
'liveTv.reorderFavorites' => 'Ordna om favoriter',
|
||||
'liveTv.favoritesLoadFailed' => 'Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => 'Gå med i pågående session',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => 'Titta från början (${minutes} min sedan)',
|
||||
'liveTv.watchLive' => 'Titta live',
|
||||
@@ -3157,11 +3173,11 @@ extension on TranslationsSv {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} lämnade',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} pausade',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} återupptog',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} spolade',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} buffrar',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} använder en äldre appversion — synkronisering är inte tillgänglig',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => 'Återupptar utan ${name}',
|
||||
'watchTogether.waitingForParticipants' => 'Väntar på att andra laddar...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => 'Väntar på ${name}...',
|
||||
|
||||
+46
-30
@@ -52,6 +52,7 @@ class TranslationsZh extends Translations with BaseTranslations<AppLocale, Trans
|
||||
@override late final _TranslationsRateSheetZh rateSheet = _TranslationsRateSheetZh._(_root);
|
||||
@override late final _TranslationsAccessibilityZh accessibility = _TranslationsAccessibilityZh._(_root);
|
||||
@override late final _TranslationsTooltipsZh tooltips = _TranslationsTooltipsZh._(_root);
|
||||
@override late final _TranslationsAudioTracksZh audioTracks = _TranslationsAudioTracksZh._(_root);
|
||||
@override late final _TranslationsVideoControlsZh videoControls = _TranslationsVideoControlsZh._(_root);
|
||||
@override late final _TranslationsMessagesZh messages = _TranslationsMessagesZh._(_root);
|
||||
@override late final _TranslationsSubtitlingStylingZh subtitlingStyling = _TranslationsSubtitlingStylingZh._(_root);
|
||||
@@ -120,7 +121,7 @@ class _TranslationsAuthZh extends TranslationsAuthEn {
|
||||
@override String get quickConnectWaiting => '等待批准…';
|
||||
@override String get quickConnectCancel => '取消';
|
||||
@override String get quickConnectExpired => 'Quick Connect 已过期。请重试。';
|
||||
@override String get localDataRecoveryRequired => '';
|
||||
@override String get localDataRecoveryRequired => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.';
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -240,8 +241,6 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
||||
@override String get libraryDensity => '媒体库密度';
|
||||
@override String get compact => '紧凑';
|
||||
@override String get comfortable => '舒适';
|
||||
@override String get tvCornerSpotlightBackdrop => '';
|
||||
@override String get tvCornerSpotlightBackdropDescription => '';
|
||||
@override String get viewMode => '视图模式';
|
||||
@override String get gridView => '网格视图';
|
||||
@override String get listView => '列表视图';
|
||||
@@ -311,7 +310,7 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
||||
@override String get watchTogetherRelay => '一起看中继服务器';
|
||||
@override String get watchTogetherRelayDescription => '设置自定义中继。所有人必须使用同一服务器。';
|
||||
@override String get watchTogetherRelayHint => 'https://my-relay.example.com';
|
||||
@override String get watchTogetherRelayInvalid => '';
|
||||
@override String get watchTogetherRelayInvalid => 'Enter a valid HTTP or HTTPS relay base URL.';
|
||||
@override String get crashReporting => '崩溃报告';
|
||||
@override String get crashReportingDescription => '发送崩溃报告以帮助改进应用';
|
||||
@override String get debugLogging => '调试日志';
|
||||
@@ -328,12 +327,10 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
||||
@override String get exportSettings => '导出设置';
|
||||
@override String get exportSettingsDescription => '将您的偏好保存到文件';
|
||||
@override String get exportSettingsSuccess => '设置已导出';
|
||||
@override String get exportSettingsFailed => '无法导出设置';
|
||||
@override String get importSettings => '导入设置';
|
||||
@override String get importSettingsDescription => '从文件恢复偏好';
|
||||
@override String get importSettingsConfirm => '这将替换您当前的设置。继续吗?';
|
||||
@override String get importSettingsSuccess => '设置已导入';
|
||||
@override String get importSettingsFailed => '无法导入设置';
|
||||
@override String get importSettingsInvalidFile => '此文件不是有效的 Plezy 设置导出';
|
||||
@override String get importSettingsNoUser => '导入设置前请先登录';
|
||||
@override String get shortcutsReset => '快捷键已重置为默认值';
|
||||
@@ -348,6 +345,7 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
||||
@override String validationErrorDuration({required Object min, required Object max, required Object unit}) => '时长必须介于 ${min} 和 ${max} ${unit} 之间';
|
||||
@override String shortcutAlreadyAssigned({required Object action}) => '快捷键已被分配给 ${action}';
|
||||
@override String shortcutUpdated({required Object action}) => '快捷键已为 ${action} 更新';
|
||||
@override String get saveFailed => 'Could not save changes. Try again.';
|
||||
@override String get autoSkip => '自动跳过';
|
||||
@override String get autoSkipIntro => '自动跳过片头';
|
||||
@override String get autoSkipIntroDescription => '几秒钟后自动跳过片头标记';
|
||||
@@ -373,7 +371,7 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
||||
@override String get downloadLocationChanged => '下载位置已更改';
|
||||
@override String get downloadLocationReset => '下载位置已重置为默认';
|
||||
@override String get downloadLocationInvalid => '所选文件夹不可写入';
|
||||
@override String get downloadLocationSelectError => '选择文件夹失败';
|
||||
@override String get downloadLocationPickerUnavailable => '此设备不支持选择文件夹';
|
||||
@override String get downloadOnWifiOnly => '仅在 WiFi 时下载';
|
||||
@override String get downloadOnWifiOnlyDescription => '使用蜂窝数据时禁止下载';
|
||||
@override String get autoRemoveWatchedDownloads => '自动移除已观看的下载';
|
||||
@@ -591,6 +589,10 @@ class _TranslationsAccessibilityZh extends TranslationsAccessibilityEn {
|
||||
@override String get hexColor => '十六进制颜色';
|
||||
@override String get expandText => '展开文本';
|
||||
@override String get collapseText => '折叠文本';
|
||||
@override String get alphabetNavigation => '字母导航';
|
||||
@override String get alphabetScrollHint => '上下滑动以按字母移动';
|
||||
@override String rowColumnPosition({required Object row, required Object rowCount, required Object column, required Object columnCount}) => '第 ${row} 行,共 ${rowCount} 行;第 ${column} 列,共 ${columnCount} 列';
|
||||
@override String rowPosition({required Object row, required Object rowCount}) => '第 ${row} 行,共 ${rowCount} 行';
|
||||
}
|
||||
|
||||
// Path: tooltips
|
||||
@@ -606,6 +608,16 @@ class _TranslationsTooltipsZh extends TranslationsTooltipsEn {
|
||||
@override String get markAsUnwatched => '标记为未观看';
|
||||
}
|
||||
|
||||
// Path: audioTracks
|
||||
class _TranslationsAudioTracksZh extends TranslationsAudioTracksEn {
|
||||
_TranslationsAudioTracksZh._(TranslationsZh root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsZh _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String track({required Object n}) => '音轨 ${n}';
|
||||
}
|
||||
|
||||
// Path: videoControls
|
||||
class _TranslationsVideoControlsZh extends TranslationsVideoControlsEn {
|
||||
_TranslationsVideoControlsZh._(TranslationsZh root) : this._root = root, super.internal(root);
|
||||
@@ -711,11 +723,11 @@ class _TranslationsMessagesZh extends TranslationsMessagesEn {
|
||||
@override String get streamInterrupted => '视频流已中断。按播放键或拖动进度条重试。';
|
||||
@override String get liveStreamInterrupted => '直播流已中断。按播放键重试。';
|
||||
@override String get fileInfoNotAvailable => '文件信息不可用';
|
||||
@override String get playbackAuthenticationRequired => '';
|
||||
@override String get playbackServerUnavailable => '';
|
||||
@override String get playbackDataInvalid => '';
|
||||
@override String get playbackCancelled => '';
|
||||
@override String get playbackFailed => '';
|
||||
@override String get playbackAuthenticationRequired => 'Sign in to the media server again to play this item.';
|
||||
@override String get playbackServerUnavailable => 'The media server is unavailable. Try again later.';
|
||||
@override String get playbackDataInvalid => 'The server returned invalid playback information.';
|
||||
@override String get playbackCancelled => 'Playback was cancelled.';
|
||||
@override String get playbackFailed => 'Playback could not be started.';
|
||||
@override String errorLoadingFileInfo({required Object error}) => '加载文件信息时出错: ${error}';
|
||||
@override String get errorLoadingSeries => '加载系列时出错';
|
||||
@override String get musicNotSupported => '尚不支持播放音乐';
|
||||
@@ -866,6 +878,7 @@ class _TranslationsProfilesZh extends TranslationsProfilesEn {
|
||||
@override String get borrowExplain => '借用另一个个人资料的连接。受 PIN 保护的个人资料需要 PIN。';
|
||||
@override String get borrowEmpty => '暂无可借用的内容。';
|
||||
@override String get borrowEmptySubtitle => '请先将 Plex 或 Jellyfin 连接到另一个个人资料。';
|
||||
@override String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
|
||||
@override String borrowFromProfile({required Object displayName}) => '来自 ${displayName}';
|
||||
@override String get borrowConnectionBorrowed => '已借用连接。';
|
||||
@override String get borrowFailed => '无法借用连接。';
|
||||
@@ -1148,7 +1161,7 @@ class _TranslationsLiveTvZh extends TranslationsLiveTvEn {
|
||||
@override String get favorites => '收藏';
|
||||
@override String get reorderFavorites => '重新排序收藏';
|
||||
@override String get favoritesLoadFailed => '无法加载收藏。请检查网络连接后重试。';
|
||||
@override String get favoritesUpdateFailed => '';
|
||||
@override String get favoritesUpdateFailed => 'Could not update favorites. Check your connection and try again.';
|
||||
@override String get joinSession => '加入正在进行的会话';
|
||||
@override String watchFromStart({required Object minutes}) => '从头观看(${minutes}分钟前开始)';
|
||||
@override String get watchLive => '观看直播';
|
||||
@@ -2146,7 +2159,7 @@ extension on TranslationsZh {
|
||||
'auth.quickConnectWaiting' => '等待批准…',
|
||||
'auth.quickConnectCancel' => '取消',
|
||||
'auth.quickConnectExpired' => 'Quick Connect 已过期。请重试。',
|
||||
'auth.localDataRecoveryRequired' => '',
|
||||
'auth.localDataRecoveryRequired' => 'Plezy could not safely recover local sign-in and pending playback data. Please sign in again.',
|
||||
'common.cancel' => '取消',
|
||||
'common.save' => '保存',
|
||||
'common.close' => '关闭',
|
||||
@@ -2230,8 +2243,6 @@ extension on TranslationsZh {
|
||||
'settings.libraryDensity' => '媒体库密度',
|
||||
'settings.compact' => '紧凑',
|
||||
'settings.comfortable' => '舒适',
|
||||
'settings.tvCornerSpotlightBackdrop' => '',
|
||||
'settings.tvCornerSpotlightBackdropDescription' => '',
|
||||
'settings.viewMode' => '视图模式',
|
||||
'settings.gridView' => '网格视图',
|
||||
'settings.listView' => '列表视图',
|
||||
@@ -2301,7 +2312,7 @@ extension on TranslationsZh {
|
||||
'settings.watchTogetherRelay' => '一起看中继服务器',
|
||||
'settings.watchTogetherRelayDescription' => '设置自定义中继。所有人必须使用同一服务器。',
|
||||
'settings.watchTogetherRelayHint' => 'https://my-relay.example.com',
|
||||
'settings.watchTogetherRelayInvalid' => '',
|
||||
'settings.watchTogetherRelayInvalid' => 'Enter a valid HTTP or HTTPS relay base URL.',
|
||||
'settings.crashReporting' => '崩溃报告',
|
||||
'settings.crashReportingDescription' => '发送崩溃报告以帮助改进应用',
|
||||
'settings.debugLogging' => '调试日志',
|
||||
@@ -2318,12 +2329,10 @@ extension on TranslationsZh {
|
||||
'settings.exportSettings' => '导出设置',
|
||||
'settings.exportSettingsDescription' => '将您的偏好保存到文件',
|
||||
'settings.exportSettingsSuccess' => '设置已导出',
|
||||
'settings.exportSettingsFailed' => '无法导出设置',
|
||||
'settings.importSettings' => '导入设置',
|
||||
'settings.importSettingsDescription' => '从文件恢复偏好',
|
||||
'settings.importSettingsConfirm' => '这将替换您当前的设置。继续吗?',
|
||||
'settings.importSettingsSuccess' => '设置已导入',
|
||||
'settings.importSettingsFailed' => '无法导入设置',
|
||||
'settings.importSettingsInvalidFile' => '此文件不是有效的 Plezy 设置导出',
|
||||
'settings.importSettingsNoUser' => '导入设置前请先登录',
|
||||
'settings.shortcutsReset' => '快捷键已重置为默认值',
|
||||
@@ -2338,6 +2347,7 @@ extension on TranslationsZh {
|
||||
'settings.validationErrorDuration' => ({required Object min, required Object max, required Object unit}) => '时长必须介于 ${min} 和 ${max} ${unit} 之间',
|
||||
'settings.shortcutAlreadyAssigned' => ({required Object action}) => '快捷键已被分配给 ${action}',
|
||||
'settings.shortcutUpdated' => ({required Object action}) => '快捷键已为 ${action} 更新',
|
||||
'settings.saveFailed' => 'Could not save changes. Try again.',
|
||||
'settings.autoSkip' => '自动跳过',
|
||||
'settings.autoSkipIntro' => '自动跳过片头',
|
||||
'settings.autoSkipIntroDescription' => '几秒钟后自动跳过片头标记',
|
||||
@@ -2363,7 +2373,7 @@ extension on TranslationsZh {
|
||||
'settings.downloadLocationChanged' => '下载位置已更改',
|
||||
'settings.downloadLocationReset' => '下载位置已重置为默认',
|
||||
'settings.downloadLocationInvalid' => '所选文件夹不可写入',
|
||||
'settings.downloadLocationSelectError' => '选择文件夹失败',
|
||||
'settings.downloadLocationPickerUnavailable' => '此设备不支持选择文件夹',
|
||||
'settings.downloadOnWifiOnly' => '仅在 WiFi 时下载',
|
||||
'settings.downloadOnWifiOnlyDescription' => '使用蜂窝数据时禁止下载',
|
||||
'settings.autoRemoveWatchedDownloads' => '自动移除已观看的下载',
|
||||
@@ -2551,10 +2561,15 @@ extension on TranslationsZh {
|
||||
'accessibility.hexColor' => '十六进制颜色',
|
||||
'accessibility.expandText' => '展开文本',
|
||||
'accessibility.collapseText' => '折叠文本',
|
||||
'accessibility.alphabetNavigation' => '字母导航',
|
||||
'accessibility.alphabetScrollHint' => '上下滑动以按字母移动',
|
||||
'accessibility.rowColumnPosition' => ({required Object row, required Object rowCount, required Object column, required Object columnCount}) => '第 ${row} 行,共 ${rowCount} 行;第 ${column} 列,共 ${columnCount} 列',
|
||||
'accessibility.rowPosition' => ({required Object row, required Object rowCount}) => '第 ${row} 行,共 ${rowCount} 行',
|
||||
'tooltips.shufflePlay' => '随机播放',
|
||||
'tooltips.playTrailer' => '播放预告片',
|
||||
'tooltips.markAsWatched' => '标记为已观看',
|
||||
'tooltips.markAsUnwatched' => '标记为未观看',
|
||||
'audioTracks.track' => ({required Object n}) => '音轨 ${n}',
|
||||
'videoControls.audioLabel' => '音频',
|
||||
'videoControls.subtitlesLabel' => '字幕',
|
||||
'videoControls.resetToZero' => '重置为 0ms',
|
||||
@@ -2641,20 +2656,20 @@ extension on TranslationsZh {
|
||||
'messages.markedAsUnwatched' => '已标记为未观看',
|
||||
'messages.markedAsWatchedOffline' => '已标记为已观看 (将在联网时同步)',
|
||||
'messages.markedAsUnwatchedOffline' => '已标记为未观看 (将在联网时同步)',
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '已自动移除: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, other: '已自动移除 ${n} 个看过的下载', ),
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'messages.autoRemovedWatchedDownload' => ({required Object title}) => '已自动移除: ${title}',
|
||||
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, other: '已自动移除 ${n} 个看过的下载', ),
|
||||
'messages.removedFromContinueWatching' => '已从继续观看中移除',
|
||||
'messages.errorLoading' => ({required Object error}) => '错误: ${error}',
|
||||
'messages.streamInterrupted' => '视频流已中断。按播放键或拖动进度条重试。',
|
||||
'messages.liveStreamInterrupted' => '直播流已中断。按播放键重试。',
|
||||
'messages.fileInfoNotAvailable' => '文件信息不可用',
|
||||
'messages.playbackAuthenticationRequired' => '',
|
||||
'messages.playbackServerUnavailable' => '',
|
||||
'messages.playbackDataInvalid' => '',
|
||||
'messages.playbackCancelled' => '',
|
||||
'messages.playbackFailed' => '',
|
||||
'messages.playbackAuthenticationRequired' => 'Sign in to the media server again to play this item.',
|
||||
'messages.playbackServerUnavailable' => 'The media server is unavailable. Try again later.',
|
||||
'messages.playbackDataInvalid' => 'The server returned invalid playback information.',
|
||||
'messages.playbackCancelled' => 'Playback was cancelled.',
|
||||
'messages.playbackFailed' => 'Playback could not be started.',
|
||||
'messages.errorLoadingFileInfo' => ({required Object error}) => '加载文件信息时出错: ${error}',
|
||||
'messages.errorLoadingSeries' => '加载系列时出错',
|
||||
'messages.musicNotSupported' => '尚不支持播放音乐',
|
||||
@@ -2769,6 +2784,7 @@ extension on TranslationsZh {
|
||||
'profiles.borrowExplain' => '借用另一个个人资料的连接。受 PIN 保护的个人资料需要 PIN。',
|
||||
'profiles.borrowEmpty' => '暂无可借用的内容。',
|
||||
'profiles.borrowEmptySubtitle' => '请先将 Plex 或 Jellyfin 连接到另一个个人资料。',
|
||||
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
|
||||
'profiles.borrowFromProfile' => ({required Object displayName}) => '来自 ${displayName}',
|
||||
'profiles.borrowConnectionBorrowed' => '已借用连接。',
|
||||
'profiles.borrowFailed' => '无法借用连接。',
|
||||
@@ -2994,7 +3010,7 @@ extension on TranslationsZh {
|
||||
'liveTv.favorites' => '收藏',
|
||||
'liveTv.reorderFavorites' => '重新排序收藏',
|
||||
'liveTv.favoritesLoadFailed' => '无法加载收藏。请检查网络连接后重试。',
|
||||
'liveTv.favoritesUpdateFailed' => '',
|
||||
'liveTv.favoritesUpdateFailed' => 'Could not update favorites. Check your connection and try again.',
|
||||
'liveTv.joinSession' => '加入正在进行的会话',
|
||||
'liveTv.watchFromStart' => ({required Object minutes}) => '从头观看(${minutes}分钟前开始)',
|
||||
'liveTv.watchLive' => '观看直播',
|
||||
@@ -3154,11 +3170,11 @@ extension on TranslationsZh {
|
||||
'watchTogether.participantLeft' => ({required Object name}) => '${name} 离开了',
|
||||
'watchTogether.participantPaused' => ({required Object name}) => '${name} 暂停了',
|
||||
'watchTogether.participantResumed' => ({required Object name}) => '${name} 继续播放了',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.participantSeeked' => ({required Object name}) => '${name} 跳转了',
|
||||
'watchTogether.participantBuffering' => ({required Object name}) => '${name} 正在缓冲',
|
||||
'watchTogether.participantNeedsUpdate' => ({required Object name}) => '${name} 正在使用较旧版本的应用,无法同步',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'watchTogether.resumingWithout' => ({required Object name}) => '不等待 ${name},继续播放',
|
||||
'watchTogether.waitingForParticipants' => '等待其他人加载...',
|
||||
'watchTogether.waitingForName' => ({required Object name}) => '正在等待 ${name}...',
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "Väntar på godkännande…",
|
||||
"quickConnectCancel": "Avbryt",
|
||||
"quickConnectExpired": "Quick Connect har gått ut. Försök igen.",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Avbryt",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "Titta Tillsammans-relay",
|
||||
"watchTogetherRelayDescription": "Ange en anpassad relay. Alla måste använda samma server.",
|
||||
"watchTogetherRelayHint": "https://min-relay.exempel.se",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "Kraschrapportering",
|
||||
"crashReportingDescription": "Skicka kraschrapporter för att förbättra appen",
|
||||
"debugLogging": "Felsökningsloggning",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "Exportera inställningar",
|
||||
"exportSettingsDescription": "Spara dina inställningar till en fil",
|
||||
"exportSettingsSuccess": "Inställningar exporterade",
|
||||
"exportSettingsFailed": "Kunde inte exportera inställningar",
|
||||
"importSettings": "Importera inställningar",
|
||||
"importSettingsDescription": "Återställ inställningar från en fil",
|
||||
"importSettingsConfirm": "Detta ersätter dina nuvarande inställningar. Fortsätta?",
|
||||
"importSettingsSuccess": "Inställningar importerade",
|
||||
"importSettingsFailed": "Kunde inte importera inställningar",
|
||||
"importSettingsInvalidFile": "Denna fil är inte en giltig Plezy-export",
|
||||
"importSettingsNoUser": "Logga in innan du importerar inställningar",
|
||||
"shortcutsReset": "Genvägar återställda till standard",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "Tiden måste vara mellan ${min} och ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Genväg redan tilldelad ${action}",
|
||||
"shortcutUpdated": "Genväg uppdaterad för ${action}",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "Auto Hoppa Över",
|
||||
"autoSkipIntro": "Hoppa Över Intro Automatiskt",
|
||||
"autoSkipIntroDescription": "Hoppa automatiskt över intro-markörer efter några sekunder",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "Nedladdningsplats ändrad",
|
||||
"downloadLocationReset": "Nedladdningsplats återställd till standard",
|
||||
"downloadLocationInvalid": "Vald mapp är inte skrivbar",
|
||||
"downloadLocationSelectError": "Kunde inte välja mapp",
|
||||
"downloadLocationPickerUnavailable": "Mappval är inte tillgängligt på den här enheten",
|
||||
"downloadOnWifiOnly": "Ladda ner endast på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Förhindra nedladdningar vid användning av mobildata",
|
||||
"autoRemoveWatchedDownloads": "Ta bort sedda nedladdningar automatiskt",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "Ljusstyrka",
|
||||
"hexColor": "Hexfärg",
|
||||
"expandText": "Expandera text",
|
||||
"collapseText": "Fäll ihop text"
|
||||
"collapseText": "Fäll ihop text",
|
||||
"alphabetNavigation": "Alfabetisk navigering",
|
||||
"alphabetScrollHint": "Svep upp eller ned för att flytta en bokstav",
|
||||
"rowColumnPosition": "Rad ${row} av ${rowCount}, kolumn ${column} av ${columnCount}",
|
||||
"rowPosition": "Rad ${row} av ${rowCount}"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Blanda uppspelning",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "Markera som sedd",
|
||||
"markAsUnwatched": "Markera som osedd"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "Ljudspår ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Ljud",
|
||||
"subtitlesLabel": "Undertexter",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "Spela nästa",
|
||||
"playButton": "Spela",
|
||||
"pauseButton": "Pausa",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "Spola bakåt ${seconds} sekunder",
|
||||
"seekForwardButton": "Spola framåt ${seconds} sekunder",
|
||||
"previousButton": "Föregående avsnitt",
|
||||
@@ -553,11 +561,11 @@
|
||||
"streamInterrupted": "Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.",
|
||||
"liveStreamInterrupted": "Livestreamen avbröts. Tryck på play för att försöka igen.",
|
||||
"fileInfoNotAvailable": "Filinformation inte tillgänglig",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "Fel vid laddning av filinformation: ${error}",
|
||||
"errorLoadingSeries": "Fel vid laddning av serie",
|
||||
"musicNotSupported": "Musikuppspelning stöds inte ännu",
|
||||
@@ -680,6 +688,7 @@
|
||||
"borrowExplain": "Låna en annan profils anslutning. PIN-skyddade profiler kräver en PIN.",
|
||||
"borrowEmpty": "Inget att låna ännu.",
|
||||
"borrowEmptySubtitle": "Anslut Plex eller Jellyfin till en annan profil först.",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "Från ${displayName}",
|
||||
"borrowConnectionBorrowed": "Anslutning lånad.",
|
||||
"borrowFailed": "Kunde inte låna anslutningen.",
|
||||
@@ -944,7 +953,7 @@
|
||||
"favorites": "Favoriter",
|
||||
"reorderFavorites": "Ordna om favoriter",
|
||||
"favoritesLoadFailed": "Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "Gå med i pågående session",
|
||||
"watchFromStart": "Titta från början (${minutes} min sedan)",
|
||||
"watchLive": "Titta live",
|
||||
|
||||
+21
-12
@@ -17,7 +17,7 @@
|
||||
"quickConnectWaiting": "等待批准…",
|
||||
"quickConnectCancel": "取消",
|
||||
"quickConnectExpired": "Quick Connect 已过期。请重试。",
|
||||
"localDataRecoveryRequired": ""
|
||||
"localDataRecoveryRequired": "Plezy could not safely recover local sign-in and pending playback data. Please sign in again."
|
||||
},
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
@@ -180,7 +180,7 @@
|
||||
"watchTogetherRelay": "一起看中继服务器",
|
||||
"watchTogetherRelayDescription": "设置自定义中继。所有人必须使用同一服务器。",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"watchTogetherRelayInvalid": "",
|
||||
"watchTogetherRelayInvalid": "Enter a valid HTTP or HTTPS relay base URL.",
|
||||
"crashReporting": "崩溃报告",
|
||||
"crashReportingDescription": "发送崩溃报告以帮助改进应用",
|
||||
"debugLogging": "调试日志",
|
||||
@@ -197,12 +197,10 @@
|
||||
"exportSettings": "导出设置",
|
||||
"exportSettingsDescription": "将您的偏好保存到文件",
|
||||
"exportSettingsSuccess": "设置已导出",
|
||||
"exportSettingsFailed": "无法导出设置",
|
||||
"importSettings": "导入设置",
|
||||
"importSettingsDescription": "从文件恢复偏好",
|
||||
"importSettingsConfirm": "这将替换您当前的设置。继续吗?",
|
||||
"importSettingsSuccess": "设置已导入",
|
||||
"importSettingsFailed": "无法导入设置",
|
||||
"importSettingsInvalidFile": "此文件不是有效的 Plezy 设置导出",
|
||||
"importSettingsNoUser": "导入设置前请先登录",
|
||||
"shortcutsReset": "快捷键已重置为默认值",
|
||||
@@ -217,6 +215,7 @@
|
||||
"validationErrorDuration": "时长必须介于 ${min} 和 ${max} ${unit} 之间",
|
||||
"shortcutAlreadyAssigned": "快捷键已被分配给 ${action}",
|
||||
"shortcutUpdated": "快捷键已为 ${action} 更新",
|
||||
"saveFailed": "Could not save changes. Try again.",
|
||||
"autoSkip": "自动跳过",
|
||||
"autoSkipIntro": "自动跳过片头",
|
||||
"autoSkipIntroDescription": "几秒钟后自动跳过片头标记",
|
||||
@@ -242,7 +241,7 @@
|
||||
"downloadLocationChanged": "下载位置已更改",
|
||||
"downloadLocationReset": "下载位置已重置为默认",
|
||||
"downloadLocationInvalid": "所选文件夹不可写入",
|
||||
"downloadLocationSelectError": "选择文件夹失败",
|
||||
"downloadLocationPickerUnavailable": "此设备不支持选择文件夹",
|
||||
"downloadOnWifiOnly": "仅在 WiFi 时下载",
|
||||
"downloadOnWifiOnlyDescription": "使用蜂窝数据时禁止下载",
|
||||
"autoRemoveWatchedDownloads": "自动移除已观看的下载",
|
||||
@@ -443,7 +442,11 @@
|
||||
"brightness": "亮度",
|
||||
"hexColor": "十六进制颜色",
|
||||
"expandText": "展开文本",
|
||||
"collapseText": "折叠文本"
|
||||
"collapseText": "折叠文本",
|
||||
"alphabetNavigation": "字母导航",
|
||||
"alphabetScrollHint": "上下滑动以按字母移动",
|
||||
"rowColumnPosition": "第 ${row} 行,共 ${rowCount} 行;第 ${column} 列,共 ${columnCount} 列",
|
||||
"rowPosition": "第 ${row} 行,共 ${rowCount} 行"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "随机播放",
|
||||
@@ -451,6 +454,9 @@
|
||||
"markAsWatched": "标记为已观看",
|
||||
"markAsUnwatched": "标记为未观看"
|
||||
},
|
||||
"audioTracks": {
|
||||
"track": "音轨 ${n}"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "音频",
|
||||
"subtitlesLabel": "字幕",
|
||||
@@ -478,6 +484,8 @@
|
||||
"playNext": "播放下一集",
|
||||
"playButton": "播放",
|
||||
"pauseButton": "暂停",
|
||||
"showPlaybackControls": "",
|
||||
"hidePlaybackControls": "",
|
||||
"seekBackwardButton": "后退 ${seconds} 秒",
|
||||
"seekForwardButton": "前进 ${seconds} 秒",
|
||||
"previousButton": "上一集",
|
||||
@@ -552,11 +560,11 @@
|
||||
"streamInterrupted": "视频流已中断。按播放键或拖动进度条重试。",
|
||||
"liveStreamInterrupted": "直播流已中断。按播放键重试。",
|
||||
"fileInfoNotAvailable": "文件信息不可用",
|
||||
"playbackAuthenticationRequired": "",
|
||||
"playbackServerUnavailable": "",
|
||||
"playbackDataInvalid": "",
|
||||
"playbackCancelled": "",
|
||||
"playbackFailed": "",
|
||||
"playbackAuthenticationRequired": "Sign in to the media server again to play this item.",
|
||||
"playbackServerUnavailable": "The media server is unavailable. Try again later.",
|
||||
"playbackDataInvalid": "The server returned invalid playback information.",
|
||||
"playbackCancelled": "Playback was cancelled.",
|
||||
"playbackFailed": "Playback could not be started.",
|
||||
"errorLoadingFileInfo": "加载文件信息时出错: ${error}",
|
||||
"errorLoadingSeries": "加载系列时出错",
|
||||
"musicNotSupported": "尚不支持播放音乐",
|
||||
@@ -679,6 +687,7 @@
|
||||
"borrowExplain": "借用另一个个人资料的连接。受 PIN 保护的个人资料需要 PIN。",
|
||||
"borrowEmpty": "暂无可借用的内容。",
|
||||
"borrowEmptySubtitle": "请先将 Plex 或 Jellyfin 连接到另一个个人资料。",
|
||||
"borrowLoadFailed": "Available connections could not be loaded. Try again.",
|
||||
"borrowFromProfile": "来自 ${displayName}",
|
||||
"borrowConnectionBorrowed": "已借用连接。",
|
||||
"borrowFailed": "无法借用连接。",
|
||||
@@ -942,7 +951,7 @@
|
||||
"favorites": "收藏",
|
||||
"reorderFavorites": "重新排序收藏",
|
||||
"favoritesLoadFailed": "无法加载收藏。请检查网络连接后重试。",
|
||||
"favoritesUpdateFailed": "",
|
||||
"favoritesUpdateFailed": "Could not update favorites. Check your connection and try again.",
|
||||
"joinSession": "加入正在进行的会话",
|
||||
"watchFromStart": "从头观看(${minutes}分钟前开始)",
|
||||
"watchLive": "观看直播",
|
||||
|
||||
@@ -59,12 +59,14 @@ mixin GridFocusNodeMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
}
|
||||
for (final key in keysToRemove) {
|
||||
final node = gridItemFocusNodes.remove(key);
|
||||
if (node != null && !node.hasFocus) {
|
||||
final node = gridItemFocusNodes[key];
|
||||
// A focused node is still borrowed by its mounted card. Keep ownership
|
||||
// and indexed identity until a later eviction or final teardown.
|
||||
if (node == null || node.hasFocus) continue;
|
||||
gridItemFocusNodes.remove(key);
|
||||
node.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void disposeGridFocusNodes() {
|
||||
for (final node in gridItemFocusNodes.values) {
|
||||
|
||||
@@ -208,8 +208,7 @@ class ShaderPreset {
|
||||
return '';
|
||||
}
|
||||
|
||||
static List<ShaderPreset> get allPresets {
|
||||
return [
|
||||
static final List<ShaderPreset> _builtInPresets = List.unmodifiable([
|
||||
none,
|
||||
nvscalerDefault,
|
||||
artcnnPreset(ArtCNNModel.c4f16, ArtCNNVariant.neutral),
|
||||
@@ -230,16 +229,15 @@ class ShaderPreset {
|
||||
anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeAA),
|
||||
anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeBB),
|
||||
anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeCA),
|
||||
];
|
||||
}
|
||||
]);
|
||||
|
||||
static ShaderPreset? fromId(String id) {
|
||||
try {
|
||||
return allPresets.firstWhere((p) => p.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
static final Map<String, ShaderPreset> _builtInPresetsById = Map.unmodifiable({
|
||||
for (final preset in _builtInPresets) preset.id: preset,
|
||||
});
|
||||
|
||||
static List<ShaderPreset> get allPresets => _builtInPresets;
|
||||
|
||||
static ShaderPreset? fromId(String id) => _builtInPresetsById[id];
|
||||
|
||||
bool get isEnabled => type != ShaderPresetType.none;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
ShaderPreset _savedPreset = ShaderPreset.none;
|
||||
ShaderPreset _currentPreset = ShaderPreset.none;
|
||||
List<ShaderPreset> _customPresets = [];
|
||||
List<ShaderPreset> _allPresets = ShaderPreset.allPresets;
|
||||
bool _initialized = false;
|
||||
|
||||
ShaderProvider() {
|
||||
@@ -26,9 +27,22 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
}
|
||||
|
||||
void _syncFromSettings(SettingsService service) {
|
||||
final customData = service.read(SettingsService.customShaderPresets);
|
||||
final customPresets = customData.map((json) => ShaderPreset.fromJson(json)).toList();
|
||||
final customPresets = <ShaderPreset>[];
|
||||
for (final json in service.read(SettingsService.customShaderPresets)) {
|
||||
try {
|
||||
final preset = ShaderPreset.fromJson(json);
|
||||
final fileName = preset.fileName;
|
||||
if (preset.type == ShaderPresetType.custom &&
|
||||
fileName != null &&
|
||||
ShaderAssetLoader.isValidCustomShaderFileName(fileName)) {
|
||||
customPresets.add(preset);
|
||||
}
|
||||
} on Object {
|
||||
// Imported settings can contain structurally invalid custom rows.
|
||||
}
|
||||
}
|
||||
_customPresets = customPresets;
|
||||
_refreshAllPresets();
|
||||
|
||||
final presetId = service.read(SettingsService.globalShaderPreset);
|
||||
_savedPreset = findPresetById(presetId) ?? ShaderPreset.none;
|
||||
@@ -47,7 +61,7 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
bool get initialized => _initialized;
|
||||
ShaderPreset get savedPreset => _savedPreset;
|
||||
ShaderPreset get currentPreset => _currentPreset;
|
||||
List<ShaderPreset> get allPresets => [...ShaderPreset.allPresets, ..._customPresets];
|
||||
List<ShaderPreset> get allPresets => _allPresets;
|
||||
List<ShaderPreset> get customPresets => _customPresets;
|
||||
bool get isShaderEnabled => _currentPreset.type != ShaderPresetType.none;
|
||||
|
||||
@@ -83,6 +97,7 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
final preset = ShaderPreset(id: id, name: displayName, type: ShaderPresetType.custom, fileName: storedFileName);
|
||||
|
||||
_customPresets.add(preset);
|
||||
_refreshAllPresets();
|
||||
await _saveCustomPresets();
|
||||
return preset;
|
||||
}
|
||||
@@ -94,6 +109,7 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
await ShaderAssetLoader.deleteCustomShader(preset.fileName!);
|
||||
}
|
||||
_customPresets.removeWhere((p) => p.id == preset.id);
|
||||
_refreshAllPresets();
|
||||
await _saveCustomPresets();
|
||||
|
||||
// Reset to none if the deleted preset was active
|
||||
@@ -102,6 +118,12 @@ class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshAllPresets() {
|
||||
_allPresets = _customPresets.isEmpty
|
||||
? ShaderPreset.allPresets
|
||||
: List.unmodifiable([...ShaderPreset.allPresets, ..._customPresets]);
|
||||
}
|
||||
|
||||
Future<void> _saveCustomPresets() async {
|
||||
final service = _settingsBinding.settings ?? await SettingsService.getInstance();
|
||||
final data = _customPresets.map((p) => p.toJson()).toList();
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/dpad_navigator.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../focus/locked_hub_controller.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_hub.dart';
|
||||
import '../media/media_item.dart';
|
||||
@@ -52,9 +53,11 @@ class CatalogItemDetailScreen extends StatefulWidget {
|
||||
|
||||
class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
|
||||
final _actionBarKey = GlobalKey<FocusableActionBarState>();
|
||||
final _backButtonFocusNode = FocusNode(debugLabel: 'catalog_detail_back');
|
||||
final _castSectionKey = GlobalKey();
|
||||
final _castStripKey = GlobalKey<CastMemberStripState>();
|
||||
final _relatedSectionKey = GlobalKey<HubSectionState>();
|
||||
final _hubFocusMemory = HubFocusMemory();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
List<FocusNode> _libraryMatchFocusNodes = const [];
|
||||
CatalogSource? _watchlistSource;
|
||||
@@ -101,6 +104,7 @@ class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_backButtonFocusNode.dispose();
|
||||
_watchlistSource?.watchlistChanges.removeListener(_onWatchlistChanged);
|
||||
for (final node in _libraryMatchFocusNodes) {
|
||||
node.dispose();
|
||||
@@ -441,6 +445,7 @@ class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
|
||||
items: [for (final item in related) item.toMediaItem()],
|
||||
size: related.length,
|
||||
),
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: Symbols.recommend_rounded,
|
||||
inset: true,
|
||||
onNavigateUp: _focusSectionAboveRelated,
|
||||
@@ -561,7 +566,7 @@ class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
|
||||
? t.explore.removeFromWatchlist
|
||||
: t.explore.addToWatchlist,
|
||||
onPressed: onWatchlist == null
|
||||
? () {}
|
||||
? null
|
||||
: () => unawaited(_toggleWatchlist()),
|
||||
),
|
||||
if (_requestSource case final SeerrCatalogSource seerr)
|
||||
@@ -608,7 +613,7 @@ class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: DesktopAppBarHelper.buildAdjustedLeading(
|
||||
const AppBarBackButton(style: BackButtonStyle.circular),
|
||||
AppBarBackButton(style: BackButtonStyle.circular, focusNode: _backButtonFocusNode),
|
||||
context: hostContext,
|
||||
)!,
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/hub_vertical_navigation.dart';
|
||||
import '../focus/locked_hub_controller.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import 'package:cached_network_image_ce/cached_network_image.dart';
|
||||
@@ -112,6 +113,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final Map<String, GlobalKey<HubSectionState>> _hubKeysByIdentity = {};
|
||||
List<GlobalKey<HubSectionState>> _orderedHubKeys = const [];
|
||||
final _tvBrowseRailKey = GlobalKey<TvBrowseRailState>();
|
||||
final _hubFocusMemory = HubFocusMemory();
|
||||
|
||||
// Hero and app bar focus
|
||||
late FocusNode _heroFocusNode;
|
||||
@@ -1045,6 +1047,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
more: _hasMoreContinueWatching,
|
||||
items: _onDeck,
|
||||
),
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: Symbols.play_circle_rounded,
|
||||
onRefresh: _discover.updateItem,
|
||||
onRemoveFromContinueWatching: _discover.refreshContinueWatching,
|
||||
@@ -1062,6 +1065,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
child: HubSection(
|
||||
key: i < _orderedHubKeys.length ? _orderedHubKeys[i] : null,
|
||||
hub: _hubs[i],
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: _getHubIcon(_hubs[i].title),
|
||||
showServerName: showServerNameOnHubs || hubsSpanMultipleServers,
|
||||
onRefresh: _discover.updateItem,
|
||||
@@ -1146,6 +1150,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
return _tvBrowseRailWidget = TvBrowseRail(
|
||||
key: _tvBrowseRailKey,
|
||||
hubs: browseHubs,
|
||||
focusMemory: _hubFocusMemory,
|
||||
showServerName: showServerName,
|
||||
iconForHub: (hub, _) => hub.id == 'continue_watching' ? Symbols.play_circle_rounded : _getHubIcon(hub.title),
|
||||
onFocusedItemChanged: _setSpotlightItem,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/hub_vertical_navigation.dart';
|
||||
import '../focus/locked_hub_controller.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/ids.dart';
|
||||
import '../media/media_hub.dart';
|
||||
@@ -54,6 +55,7 @@ class ExploreScreenState extends State<ExploreScreen>
|
||||
final _sourceMenuKey = GlobalKey<AppMenuButtonState<CatalogSourceId>>();
|
||||
|
||||
final _tvBrowseRailKey = GlobalKey<TvBrowseRailState>();
|
||||
final _hubFocusMemory = HubFocusMemory();
|
||||
final TvSpotlightController _spotlight = TvSpotlightController();
|
||||
|
||||
@override
|
||||
@@ -302,6 +304,7 @@ class ExploreScreenState extends State<ExploreScreen>
|
||||
child: HubSection(
|
||||
key: _orderedHubKeys[i],
|
||||
hub: rowHubs[i].hub,
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: _rowIcon(rowHubs[i].row),
|
||||
loadMoreItems: rowHubs[i].hub.more ? () => _explore.loadAllForHub(rowHubs[i]) : null,
|
||||
onVerticalNavigation: (isUp) => _handleVerticalNavigation(i, isUp),
|
||||
@@ -431,6 +434,7 @@ class ExploreScreenState extends State<ExploreScreen>
|
||||
child: TvBrowseRail(
|
||||
key: _tvBrowseRailKey,
|
||||
hubs: tvHubs,
|
||||
focusMemory: _hubFocusMemory,
|
||||
iconForHub: (hub, _) => _rowIcon(_rowForHub(hub)?.row),
|
||||
onFocusedItemChanged: _setSpotlightItem,
|
||||
loadMoreItems: (hub) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_playlist.dart';
|
||||
import '../mixins/grid_focus_node_mixin.dart';
|
||||
@@ -195,6 +196,7 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
||||
key: Key(_idForItem(item)),
|
||||
item: item,
|
||||
focusNode: focusNode,
|
||||
semanticValue: _semanticPosition(position),
|
||||
disableScale: position.disableScale,
|
||||
onRefresh: onRefresh,
|
||||
collectionId: collectionId,
|
||||
@@ -233,7 +235,8 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
||||
final fullCardLayout = PlatformDetector.isTV() && svc.read(SettingsService.tvFullCardLayout);
|
||||
final useFullCardLayout = fullCardLayout && shape != CardShape.square;
|
||||
|
||||
Widget buildTile(int index, {required bool inFirstRow, required bool disableScale}) {
|
||||
Widget buildTile(MediaCardSliverPosition position) {
|
||||
final index = position.index;
|
||||
final item = itemAt(index);
|
||||
if (item == null) {
|
||||
onSkeletonVisible?.call(index);
|
||||
@@ -244,13 +247,14 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
||||
key: Key(item.id),
|
||||
item: item,
|
||||
focusNode: focusNode,
|
||||
disableScale: disableScale,
|
||||
semanticValue: _semanticPosition(position),
|
||||
disableScale: position.disableScale,
|
||||
onRefresh: onRefresh,
|
||||
collectionId: collectionId,
|
||||
onListRefresh: onListRefresh,
|
||||
fullBleedImage: useFullCardLayout && !disableScale,
|
||||
fullBleedImage: useFullCardLayout && position.isGrid,
|
||||
cardShapeOverride: shape,
|
||||
onNavigateUp: inFirstRow ? navigateToAppBar : null,
|
||||
onNavigateUp: position.isFirstRow ? navigateToAppBar : null,
|
||||
onBack: handleBackFromContent,
|
||||
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
||||
);
|
||||
@@ -263,10 +267,23 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
||||
padding: const EdgeInsets.all(8),
|
||||
fullBleedImage: useFullCardLayout,
|
||||
shape: shape,
|
||||
itemBuilder: (context, position) =>
|
||||
buildTile(position.index, inFirstRow: position.isFirstRow, disableScale: position.disableScale),
|
||||
itemBuilder: (context, position) => buildTile(position),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _semanticPosition(MediaCardSliverPosition position) {
|
||||
if (!position.isGrid) {
|
||||
return t.accessibility.rowPosition(row: position.index + 1, rowCount: position.itemCount);
|
||||
}
|
||||
|
||||
final rowCount = (position.itemCount + position.columnCount - 1) ~/ position.columnCount;
|
||||
return t.accessibility.rowColumnPosition(
|
||||
row: position.index ~/ position.columnCount + 1,
|
||||
rowCount: rowCount,
|
||||
column: position.index % position.columnCount + 1,
|
||||
columnCount: position.columnCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../media/library_first_character.dart';
|
||||
import '../../widgets/clickable_cursor.dart';
|
||||
import 'alpha_jump_helper.dart';
|
||||
@@ -217,8 +218,13 @@ class _AlphaJumpBarState extends State<AlphaJumpBar> {
|
||||
final markerSize = (letterSlotHeight - 2).clamp(10.0, 18.0).toDouble();
|
||||
final fontSize = (letterSlotHeight * 0.58).clamp(7.0, 10.0).toDouble();
|
||||
|
||||
return ClickableCursor(
|
||||
return Semantics(
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
label: t.accessibility.alphabetNavigation,
|
||||
child: ClickableCursor(
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: (details) {
|
||||
final idx = _letterIndexFromDy(details.localPosition.dy, constraints.maxHeight);
|
||||
@@ -264,7 +270,16 @@ class _AlphaJumpBarState extends State<AlphaJumpBar> {
|
||||
letterColor = colorScheme.onSurface;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: isCurrent || isHighlighted,
|
||||
label: letter,
|
||||
excludeSemantics: true,
|
||||
onTap: () {
|
||||
setState(() => _highlightedIndex = i);
|
||||
_jumpToLetter(letter);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: letterSlotHeight,
|
||||
child: Center(
|
||||
child: Container(
|
||||
@@ -282,11 +297,13 @@ class _AlphaJumpBarState extends State<AlphaJumpBar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../media/library_first_character.dart';
|
||||
import 'alpha_jump_helper.dart';
|
||||
|
||||
@@ -141,16 +142,44 @@ class _AlphaScrollHandleState extends State<AlphaScrollHandle> with SingleTicker
|
||||
_scheduleHide();
|
||||
}
|
||||
|
||||
int get _semanticLetterIndex {
|
||||
if (_helper.letters.isEmpty) return -1;
|
||||
final index = _helper.letters.indexOf(_dragLetter ?? widget.currentLetter);
|
||||
return index < 0 ? 0 : index;
|
||||
}
|
||||
|
||||
void _stepSemantics(int delta) {
|
||||
final currentIndex = _semanticLetterIndex;
|
||||
if (currentIndex < 0) return;
|
||||
final targetIndex = (currentIndex + delta).clamp(0, _helper.letters.length - 1);
|
||||
if (targetIndex == currentIndex) return;
|
||||
final letter = _helper.letters[targetIndex];
|
||||
_show();
|
||||
_scheduleHide();
|
||||
widget.onJump(_helper.indexForLetter(letter) ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
final semanticIndex = _semanticLetterIndex;
|
||||
return Semantics(
|
||||
label: t.accessibility.alphabetNavigation,
|
||||
value: _dragLetter ?? widget.currentLetter,
|
||||
hint: t.accessibility.alphabetScrollHint,
|
||||
increasedValue: semanticIndex >= 0 && semanticIndex < _helper.letters.length - 1
|
||||
? _helper.letters[semanticIndex + 1]
|
||||
: null,
|
||||
decreasedValue: semanticIndex > 0 ? _helper.letters[semanticIndex - 1] : null,
|
||||
onIncrease: semanticIndex >= 0 && semanticIndex < _helper.letters.length - 1 ? () => _stepSemantics(1) : null,
|
||||
onDecrease: semanticIndex > 0 ? () => _stepSemantics(-1) : null,
|
||||
child: AnimatedBuilder(
|
||||
animation: _opacityController,
|
||||
builder: (context, child) {
|
||||
final opacity = _opacityController.value;
|
||||
// Prevent stealing taps when fully hidden
|
||||
if (opacity == 0.0) return const SizedBox.shrink();
|
||||
|
||||
return Opacity(opacity: opacity, child: child);
|
||||
return IgnorePointer(
|
||||
ignoring: opacity == 0.0,
|
||||
child: Opacity(opacity: opacity, child: child),
|
||||
);
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -164,7 +193,6 @@ class _AlphaScrollHandleState extends State<AlphaScrollHandle> with SingleTicker
|
||||
final handleTop = usableHeight > 0 ? (fraction * usableHeight) : 0.0;
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SizedBox(
|
||||
width: _touchTargetWidth + _bubbleSize + _bubbleMarginRight,
|
||||
height: trackHeight,
|
||||
@@ -180,6 +208,7 @@ class _AlphaScrollHandleState extends State<AlphaScrollHandle> with SingleTicker
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeUpDown,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onVerticalDragStart: _onDragStart,
|
||||
onVerticalDragUpdate: _onDragUpdate,
|
||||
@@ -224,6 +253,7 @@ class _AlphaScrollHandleState extends State<AlphaScrollHandle> with SingleTicker
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../focus/hub_vertical_navigation.dart';
|
||||
import '../../../focus/locked_hub_controller.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../media/media_hub.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
@@ -50,9 +51,18 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
final List<GlobalKey<HubSectionState>> _hubKeys = [];
|
||||
final _tvBrowseRailKey = GlobalKey<TvBrowseRailState>();
|
||||
final TvSpotlightController _spotlight = TvSpotlightController();
|
||||
HubFocusMemory _hubFocusMemory = HubFocusMemory();
|
||||
|
||||
void _setSpotlightItem(MediaItem item) => _spotlight.select(item);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LibraryRecommendedTab oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.library.globalKey != widget.library.globalKey) {
|
||||
_hubFocusMemory = HubFocusMemory();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_spotlight.dispose();
|
||||
@@ -305,6 +315,7 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
return HubSection(
|
||||
key: index < _hubKeys.length ? _hubKeys[index] : null,
|
||||
hub: hub,
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: _getHubIcon(hub),
|
||||
isInContinueWatching: isContinueWatching,
|
||||
usesContinueWatchingAction: usesContinueWatchingAction,
|
||||
@@ -339,6 +350,7 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
child: TvBrowseRail(
|
||||
key: _tvBrowseRailKey,
|
||||
hubs: tvHubs,
|
||||
focusMemory: _hubFocusMemory,
|
||||
iconForHub: (hub, _) => _getHubIcon(hub),
|
||||
onFocusedItemChanged: _setSpotlightItem,
|
||||
onRefresh: updateItem,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../focus/hub_vertical_navigation.dart';
|
||||
import '../../../focus/locked_hub_controller.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../media/media_hub.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
@@ -39,6 +40,7 @@ class WhatsOnTabState extends State<WhatsOnTab>
|
||||
Timer? _refreshTimer;
|
||||
final Map<String, GlobalKey<HubSectionState>> _hubKeysById = {};
|
||||
List<GlobalKey<HubSectionState>> _hubKeys = [];
|
||||
final _hubFocusMemory = HubFocusMemory();
|
||||
bool _refreshRequested = true;
|
||||
bool _tickerEnabled = false;
|
||||
bool _appRefreshActive = true;
|
||||
@@ -210,6 +212,7 @@ class WhatsOnTabState extends State<WhatsOnTab>
|
||||
return HubSection(
|
||||
key: _hubKeys[index],
|
||||
hub: hub.mediaHub,
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: Symbols.live_tv_rounded,
|
||||
cardSizing: HubCardSizing.grid,
|
||||
episodePosterModeOverride: EpisodePosterMode.seriesPoster,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import '../media/ids.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image_ce/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -23,6 +22,7 @@ import '../focus/dpad_select_long_press_controller.dart';
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/focusable_wrapper.dart';
|
||||
import '../focus/hub_vertical_navigation.dart';
|
||||
import '../focus/locked_hub_controller.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../widgets/cast_member_strip.dart';
|
||||
@@ -288,6 +288,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
bool _hasLoadedExtras = false;
|
||||
bool _hasLoadedRelatedHubs = false;
|
||||
final _tvDetailRailKey = GlobalKey<TvBrowseRailState>();
|
||||
final _hubFocusMemory = HubFocusMemory();
|
||||
PageRoute<dynamic>? _route;
|
||||
RouteObserver<PageRoute<dynamic>>? _routeObserver;
|
||||
late final ScrollController _scrollController;
|
||||
@@ -361,6 +362,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
late final FocusNode _playButtonFocusNode;
|
||||
late final FocusNode _ratingChipFocusNode;
|
||||
late final FocusNode _backButtonFocusNode;
|
||||
final _extrasSelectLongPress = DpadSelectLongPressController();
|
||||
|
||||
// Context menu key for the three-dots button
|
||||
@@ -687,6 +689,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
_extrasFocusNode.addListener(_handleExtrasFocusChange);
|
||||
_playButtonFocusNode = FocusNode(debugLabel: 'play_button');
|
||||
_ratingChipFocusNode = FocusNode(debugLabel: 'rating_chip');
|
||||
_backButtonFocusNode = FocusNode(debugLabel: 'media_detail_back');
|
||||
_overviewFocusNode = FocusNode(debugLabel: 'overview');
|
||||
_infoRowsFocusNode = FocusNode(debugLabel: 'info_rows');
|
||||
_loadFullMetadata();
|
||||
@@ -881,6 +884,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
_focusedExtraIndexNotifier.dispose();
|
||||
_playButtonFocusNode.dispose();
|
||||
_ratingChipFocusNode.dispose();
|
||||
_backButtonFocusNode.dispose();
|
||||
_overviewFocusNode.dispose();
|
||||
_infoRowsFocusNode.dispose();
|
||||
_extrasSelectLongPress.dispose();
|
||||
@@ -1190,20 +1194,23 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
required ImageType imageType,
|
||||
Alignment alignment = Alignment.center,
|
||||
Widget Function(BuildContext, String, dynamic)? errorWidget,
|
||||
Widget Function(BuildContext, String)? placeholder,
|
||||
}) {
|
||||
if (!widget.isOffline || _metadata.serverId == null) return null;
|
||||
|
||||
for (final artworkPath in artworkPaths) {
|
||||
final localPath = _offlineArtworkLocalPath(context, artworkPath);
|
||||
final localPath = _offlineArtworkCandidatePath(context, artworkPath);
|
||||
if (localPath == null) continue;
|
||||
|
||||
return OptimizedMediaImage(
|
||||
client: null,
|
||||
imagePath: null,
|
||||
localFilePath: localPath,
|
||||
cacheMissingLocalFile: true,
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
imageType: imageType,
|
||||
placeholder: placeholder,
|
||||
errorWidget: errorWidget,
|
||||
);
|
||||
}
|
||||
@@ -1211,11 +1218,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _offlineArtworkLocalPath(BuildContext context, String? artworkPath) {
|
||||
String? _offlineArtworkCandidatePath(BuildContext context, String? artworkPath) {
|
||||
if (!widget.isOffline || _metadata.serverId == null) return null;
|
||||
final localPath = context.read<DownloadProvider>().getArtworkLocalPath(ServerId(_metadata.serverId!), artworkPath);
|
||||
if (localPath == null || !File(localPath).existsSync()) return null;
|
||||
return localPath;
|
||||
return context.read<DownloadProvider>().getArtworkLocalPath(ServerId(_metadata.serverId!), artworkPath);
|
||||
}
|
||||
|
||||
String _syncRuleKeyForMetadata(BuildContext context, DownloadProvider downloadProvider, MediaItem metadata) {
|
||||
@@ -2235,6 +2240,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
artworkPaths: [posterPath],
|
||||
fit: BoxFit.cover,
|
||||
imageType: ImageType.poster,
|
||||
placeholder: (context, url) => const PlaceholderContainer(),
|
||||
errorWidget: (context, url, error) => const PlaceholderContainer(),
|
||||
);
|
||||
topImage = SizedBox(
|
||||
@@ -3044,6 +3050,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final backButton = AppBarBackButton(
|
||||
style: BackButtonStyle.plain,
|
||||
onPressed: () => Navigator.pop(context, _watchStateChanged),
|
||||
focusNode: _backButtonFocusNode,
|
||||
);
|
||||
final loading = ListenableBuilder(
|
||||
listenable: FullscreenStateManager(),
|
||||
@@ -3219,6 +3226,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
HubSection(
|
||||
key: _relatedHubKeys[i],
|
||||
hub: _relatedHubs[i],
|
||||
focusMemory: _hubFocusMemory,
|
||||
icon: _getRelatedHubIcon(_relatedHubs[i]),
|
||||
inset: true,
|
||||
onVerticalNavigation: (isUp) => _handleRelatedHubNavigation(i, isUp),
|
||||
@@ -3294,6 +3302,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
AppBarBackButton(
|
||||
style: BackButtonStyle.circular,
|
||||
onPressed: () => Navigator.pop(context, _watchStateChanged),
|
||||
focusNode: _backButtonFocusNode,
|
||||
),
|
||||
context: context,
|
||||
)!,
|
||||
@@ -3353,6 +3362,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
AppBarBackButton(
|
||||
style: BackButtonStyle.circular,
|
||||
onPressed: () => Navigator.pop(context, _watchStateChanged),
|
||||
focusNode: _backButtonFocusNode,
|
||||
),
|
||||
context: context,
|
||||
)!,
|
||||
@@ -3365,6 +3375,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
child: TvBrowseRail(
|
||||
key: _tvDetailRailKey,
|
||||
hubs: detailHubs,
|
||||
focusMemory: _hubFocusMemory,
|
||||
iconForHub: _getTvDetailHubIcon,
|
||||
onFocusedHubItemChanged: _handleTvDetailFocusedRailItemChanged,
|
||||
onRefresh: (source) => unawaited(_refreshItemInPlace(source)),
|
||||
@@ -3399,7 +3410,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
item: metadata,
|
||||
client: _getArtworkMediaClient(context),
|
||||
showInfo: false,
|
||||
localArtworkPathResolver: widget.isOffline ? (path) => _offlineArtworkLocalPath(context, path) : null,
|
||||
localArtworkPathResolver: widget.isOffline
|
||||
? (path) => _offlineArtworkCandidatePath(context, path)
|
||||
: null,
|
||||
allowNetwork: !widget.isOffline,
|
||||
),
|
||||
_buildTvDetailRevealGate(revealContent, handleBack),
|
||||
@@ -3642,6 +3655,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
fit: BoxFit.contain,
|
||||
alignment: .centerLeft,
|
||||
imageType: ImageType.heroLogo,
|
||||
placeholder: (context, url) => titleFallback(context),
|
||||
errorWidget: (context, url, error) => titleFallback(context),
|
||||
);
|
||||
if (localArtwork != null) return localArtwork;
|
||||
@@ -4184,7 +4198,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
fallbackImagePaths: heroArtPaths,
|
||||
client: _getArtworkMediaClient(context),
|
||||
localArtworkPathResolver: widget.isOffline
|
||||
? (path) => _offlineArtworkLocalPath(context, path)
|
||||
? (path) => _offlineArtworkCandidatePath(context, path)
|
||||
: null,
|
||||
allowNetwork: !widget.isOffline,
|
||||
width: size.width,
|
||||
|
||||
@@ -60,7 +60,22 @@ class _BorrowConnectionScreenState extends State<BorrowConnectionScreen> {
|
||||
_candidatesFuture = _loadCandidates();
|
||||
}
|
||||
|
||||
void _retryCandidates() {
|
||||
setState(() {
|
||||
_candidatesFuture = _loadCandidates();
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<_BorrowCandidate>> _loadCandidates() async {
|
||||
try {
|
||||
return await _loadCandidatesUnchecked();
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Borrow candidate load failed', error: error, stackTrace: stackTrace);
|
||||
Error.throwWithStackTrace(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<_BorrowCandidate>> _loadCandidatesUnchecked() async {
|
||||
final pcRegistry = context.read<ProfileConnectionRegistry>();
|
||||
final connRegistry = context.read<ConnectionRegistry>();
|
||||
final profileRegistry = context.read<ProfileRegistry>();
|
||||
@@ -159,29 +174,31 @@ class _BorrowConnectionScreenState extends State<BorrowConnectionScreen> {
|
||||
return FutureBuilder<List<_BorrowCandidate>>(
|
||||
future: _candidatesFuture,
|
||||
builder: (context, snapshot) {
|
||||
final candidates = snapshot.data ?? const <_BorrowCandidate>[];
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.profiles.borrowAddTo(displayName: widget.targetProfile.displayName)),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(t.profiles.borrowExplain, style: Theme.of(context).textTheme.bodySmall),
|
||||
late final Widget candidateSliver;
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
candidateSliver = LoadingIndicatorBox.sliver;
|
||||
} else if (snapshot.hasError) {
|
||||
candidateSliver = SliverFillRemaining(
|
||||
child: ErrorStateWidget(
|
||||
message: t.profiles.borrowLoadFailed,
|
||||
onRetry: _retryCandidates,
|
||||
actionAutofocus: true,
|
||||
actionUseBackgroundFocus: true,
|
||||
),
|
||||
),
|
||||
if (snapshot.connectionState != ConnectionState.done)
|
||||
LoadingIndicatorBox.sliver
|
||||
else if (candidates.isEmpty)
|
||||
SliverFillRemaining(
|
||||
);
|
||||
} else {
|
||||
final candidates = snapshot.requireData;
|
||||
if (candidates.isEmpty) {
|
||||
candidateSliver = SliverFillRemaining(
|
||||
child: EmptyStateWidget(
|
||||
message: t.profiles.borrowEmpty,
|
||||
subtitle: t.profiles.borrowEmptySubtitle,
|
||||
icon: Symbols.share_rounded,
|
||||
iconSize: 48,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
);
|
||||
} else {
|
||||
candidateSliver = SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final cand = candidates[index];
|
||||
// M3E connected-group geometry: large outer corners, small
|
||||
@@ -206,7 +223,20 @@ class _BorrowConnectionScreenState extends State<BorrowConnectionScreen> {
|
||||
),
|
||||
);
|
||||
}, childCount: candidates.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.profiles.borrowAddTo(displayName: widget.targetProfile.displayName)),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(t.profiles.borrowExplain, style: Theme.of(context).textTheme.bodySmall),
|
||||
),
|
||||
),
|
||||
candidateSliver,
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -421,12 +421,15 @@ class _TvPinInputState extends State<_TvPinInput> with ControllerDisposerMixin {
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _onMobilePinChanged(String value) {
|
||||
final digitsOnly = value.replaceAll(RegExp(r'\D'), '');
|
||||
final pin = digitsOnly.length > 4 ? digitsOnly.substring(0, 4) : digitsOnly;
|
||||
final pin = switch (digitsOnly.length) {
|
||||
<= 4 => digitsOnly,
|
||||
_ => digitsOnly.substring(0, expandToGraphemeRange(digitsOnly, const TextRange(start: 0, end: 4)).end),
|
||||
};
|
||||
if (pin != value) {
|
||||
_mobileController.value = TextEditingValue(
|
||||
text: pin,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -11,7 +13,7 @@ import '../../widgets/dialog_action_button.dart';
|
||||
class HotKeyRecorderWidget extends StatefulWidget {
|
||||
final String actionName;
|
||||
final HotKey? currentHotKey;
|
||||
final Function(HotKey) onHotKeyRecorded;
|
||||
final FutureOr<void> Function(HotKey?) onHotKeyRecorded;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const HotKeyRecorderWidget({
|
||||
@@ -29,6 +31,8 @@ class HotKeyRecorderWidget extends StatefulWidget {
|
||||
class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
HotKey? _recordedHotKey;
|
||||
bool _isCapturing = false;
|
||||
bool _hasPendingEdit = false;
|
||||
bool _isSaving = false;
|
||||
final _recorderFocusNode = FocusNode(debugLabel: 'HotKeyRecorder.record');
|
||||
final _clearFocusNode = FocusNode(debugLabel: 'HotKeyRecorder.clear');
|
||||
final _cancelFocusNode = FocusNode(debugLabel: 'HotKeyRecorder.cancel');
|
||||
@@ -50,6 +54,7 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
}
|
||||
|
||||
void _startCapturing() {
|
||||
if (_isSaving) return;
|
||||
setState(() => _isCapturing = true);
|
||||
_recorderFocusNode.requestFocus();
|
||||
}
|
||||
@@ -57,6 +62,7 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
void _handleHotKeyRecorded(HotKey hotKey) {
|
||||
setState(() {
|
||||
_recordedHotKey = hotKey;
|
||||
_hasPendingEdit = true;
|
||||
_isCapturing = false;
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -65,19 +71,42 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
}
|
||||
|
||||
void _clearShortcut() {
|
||||
if (_isSaving) return;
|
||||
setState(() {
|
||||
_recordedHotKey = null;
|
||||
_hasPendingEdit = true;
|
||||
_isCapturing = false;
|
||||
});
|
||||
_recorderFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
void _cancel() {
|
||||
if (!_isSaving) widget.onCancel();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final canSave = (_recordedHotKey != null || _hasPendingEdit) && !_isCapturing && !_isSaving;
|
||||
if (!canSave) return;
|
||||
|
||||
final hotkey = _recordedHotKey;
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
await widget.onHotKeyRecorded(hotkey);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasShortcut = _recordedHotKey != null;
|
||||
final canEdit = !_isSaving;
|
||||
final canSave = (hasShortcut || _hasPendingEdit) && !_isCapturing && !_isSaving;
|
||||
final recordLabel = _isCapturing ? t.hotkeys.recordingShortcut : t.hotkeys.pressToRecord;
|
||||
|
||||
return AlertDialog(
|
||||
return PopScope(
|
||||
canPop: !_isSaving,
|
||||
child: AlertDialog(
|
||||
title: Text(t.hotkeys.setShortcutFor(actionName: widget.actionName)),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
@@ -97,15 +126,15 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
child: FocusableWrapper(
|
||||
focusNode: _recorderFocusNode,
|
||||
autofocus: true,
|
||||
onSelect: _startCapturing,
|
||||
onBack: widget.onCancel,
|
||||
onNavigateRight: hasShortcut ? _clearFocusNode.requestFocus : null,
|
||||
onNavigateDown: (hasShortcut ? _saveFocusNode : _cancelFocusNode).requestFocus,
|
||||
onSelect: canEdit ? _startCapturing : null,
|
||||
onBack: _cancel,
|
||||
onNavigateRight: canEdit && hasShortcut ? _clearFocusNode.requestFocus : null,
|
||||
onNavigateDown: (canSave ? _saveFocusNode : _cancelFocusNode).requestFocus,
|
||||
semanticLabel: recordLabel,
|
||||
descendantsAreFocusable: false,
|
||||
useBackgroundFocus: true,
|
||||
child: GestureDetector(
|
||||
onTap: _startCapturing,
|
||||
onTap: canEdit ? _startCapturing : null,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
@@ -115,7 +144,7 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
),
|
||||
child: HotKeyRecorder(
|
||||
initalHotKey: _recordedHotKey,
|
||||
enabled: _isCapturing,
|
||||
enabled: _isCapturing && canEdit,
|
||||
placeholder: Text(recordLabel),
|
||||
onHotKeyRecorded: _handleHotKeyRecorded,
|
||||
),
|
||||
@@ -127,14 +156,14 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
const SizedBox(width: 8),
|
||||
FocusableButton(
|
||||
focusNode: _clearFocusNode,
|
||||
onPressed: _clearShortcut,
|
||||
onBack: widget.onCancel,
|
||||
onPressed: canEdit ? _clearShortcut : null,
|
||||
onBack: _cancel,
|
||||
onNavigateLeft: _recorderFocusNode.requestFocus,
|
||||
onNavigateDown: _saveFocusNode.requestFocus,
|
||||
autoScroll: false,
|
||||
child: IconButton(
|
||||
icon: const AppIcon(Symbols.backspace_rounded, fill: 1, size: 18),
|
||||
onPressed: _clearShortcut,
|
||||
onPressed: canEdit ? _clearShortcut : null,
|
||||
padding: .zero,
|
||||
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
|
||||
tooltip: t.hotkeys.clearShortcut,
|
||||
@@ -146,9 +175,9 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
recordLabel,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -158,22 +187,23 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
actions: [
|
||||
DialogActionButton(
|
||||
focusNode: _cancelFocusNode,
|
||||
onPressed: widget.onCancel,
|
||||
onBack: widget.onCancel,
|
||||
onPressed: canEdit ? _cancel : null,
|
||||
onBack: _cancel,
|
||||
onNavigateUp: _recorderFocusNode.requestFocus,
|
||||
onNavigateRight: _saveFocusNode.requestFocus,
|
||||
onNavigateRight: canSave ? _saveFocusNode.requestFocus : null,
|
||||
label: t.common.cancel,
|
||||
),
|
||||
DialogActionButton(
|
||||
focusNode: _saveFocusNode,
|
||||
onPressed: hasShortcut ? () => widget.onHotKeyRecorded(_recordedHotKey!) : null,
|
||||
onBack: widget.onCancel,
|
||||
onPressed: canSave ? _save : null,
|
||||
onBack: _cancel,
|
||||
onNavigateUp: _recorderFocusNode.requestFocus,
|
||||
onNavigateLeft: _cancelFocusNode.requestFocus,
|
||||
label: t.common.save,
|
||||
isPrimary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/hotkey_model.dart';
|
||||
import '../../services/keyboard_shortcuts_service.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../services/shader_service.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
@@ -58,11 +60,11 @@ class KeyboardShortcutsScreen extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
keyboardService.formatHotkey(hotkeys[action]!),
|
||||
keyboardService.formatHotkey(hotkeys[action]),
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
onTap: () => _editHotkey(context, action, hotkeys[action]!),
|
||||
onTap: () => _editHotkey(context, action, hotkeys[action]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -79,37 +81,42 @@ class KeyboardShortcutsScreen extends StatelessWidget {
|
||||
if (context.mounted) showSuccessSnackBar(context, t.settings.shortcutsReset);
|
||||
}
|
||||
|
||||
void _editHotkey(BuildContext screenContext, String action, HotKey currentHotkey) {
|
||||
void _editHotkey(BuildContext screenContext, String action, HotKey? currentHotkey) {
|
||||
final actionId = action;
|
||||
final actionName = keyboardService.getActionDisplayName(actionId);
|
||||
showScopedDialog<void>(
|
||||
context: screenContext,
|
||||
builder: (BuildContext context) {
|
||||
return HotKeyRecorderWidget(
|
||||
actionName: keyboardService.getActionDisplayName(action),
|
||||
actionName: actionName,
|
||||
currentHotKey: currentHotkey,
|
||||
onHotKeyRecorded: (newHotkey) async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
// Check for conflicts
|
||||
if (newHotkey != null) {
|
||||
final existingAction = keyboardService.getActionForHotkey(newHotkey);
|
||||
if (existingAction != null && existingAction != action) {
|
||||
if (existingAction != null && existingAction != actionId) {
|
||||
navigator.pop();
|
||||
showErrorSnackBar(
|
||||
context,
|
||||
screenContext,
|
||||
t.settings.shortcutAlreadyAssigned(action: keyboardService.getActionDisplayName(existingAction)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the new hotkey
|
||||
await keyboardService.setHotkey(action, newHotkey);
|
||||
try {
|
||||
await keyboardService.setHotkey(actionId, newHotkey);
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
appLogger.e('Failed to update keyboard shortcut', error: error, stackTrace: stackTrace);
|
||||
if (screenContext.mounted) showErrorSnackBar(screenContext, t.common.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
navigator.pop();
|
||||
|
||||
if (screenContext.mounted) {
|
||||
showSuccessSnackBar(
|
||||
screenContext,
|
||||
t.settings.shortcutUpdated(action: keyboardService.getActionDisplayName(action)),
|
||||
);
|
||||
showSuccessSnackBar(screenContext, t.settings.shortcutUpdated(action: actionName));
|
||||
}
|
||||
},
|
||||
onCancel: () => Navigator.pop(context),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
@@ -9,6 +11,8 @@ import '../../i18n/strings.g.dart';
|
||||
import '../../mixins/controller_disposer_mixin.dart';
|
||||
import '../../models/mpv_config_models.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/debouncer.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../mixins/settings_effect_mixin.dart';
|
||||
@@ -35,27 +39,145 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
);
|
||||
final _savePresetFocusNode = FocusNode();
|
||||
final _textFieldFocusNode = FocusNode();
|
||||
final _saveDebouncer = Debouncer(const Duration(milliseconds: 400));
|
||||
String _persistedText = '';
|
||||
int _revision = 0;
|
||||
int _persistedRevision = 0;
|
||||
_QueuedMpvConfig? _pendingSave;
|
||||
_QueuedMpvConfig? _activeSave;
|
||||
Future<bool>? _drainFuture;
|
||||
bool _isLeaving = false;
|
||||
bool _allowPop = false;
|
||||
bool _disposing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Sync the editor when the pref is mutated externally (e.g. loadMpvPreset).
|
||||
// Skip when the listener fires for the same value the controller already
|
||||
// holds — avoids fighting user-typed text mid-edit.
|
||||
bindEffect<String>(SettingsService.mpvConfigText, (v) {
|
||||
if (_textController.text != v) _textController.text = v;
|
||||
}, fireImmediately: false);
|
||||
_persistedText = _textController.text;
|
||||
_textFieldFocusNode.addListener(_handleTextFieldFocusChanged);
|
||||
// Keep a clean editor synchronized with imports, reset, and other
|
||||
// settings producers without allowing a completed local write to replace
|
||||
// a newer queued edit.
|
||||
bindEffect<String>(SettingsService.mpvConfigText, _handlePersistedText, fireImmediately: false);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposing = true;
|
||||
_saveDebouncer.dispose();
|
||||
_textFieldFocusNode.removeListener(_handleTextFieldFocusChanged);
|
||||
if (_pendingSave != null || _drainFuture != null) {
|
||||
unawaited(_flushPending());
|
||||
}
|
||||
_savePresetFocusNode.dispose();
|
||||
_textFieldFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveText() async {
|
||||
await _settingsService.write(SettingsService.mpvConfigText, _textController.text);
|
||||
bool get _hasUnsavedWork => _pendingSave != null || _activeSave != null || _drainFuture != null;
|
||||
|
||||
void _handleTextFieldFocusChanged() {
|
||||
if (!_textFieldFocusNode.hasFocus) {
|
||||
unawaited(_flushPending());
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePersistedText(String value) {
|
||||
final active = _activeSave;
|
||||
if (active != null && active.text == value) return;
|
||||
if (active == null && _pendingSave == null && _textController.text == value) {
|
||||
_persistedText = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// An import/reset/other producer wins when observed. An in-flight local
|
||||
// write cannot be cancelled, so queue the external value behind it to
|
||||
// ensure that obsolete write cannot become the final persisted value.
|
||||
_saveDebouncer.cancel();
|
||||
final revision = ++_revision;
|
||||
_persistedText = value;
|
||||
_persistedRevision = revision;
|
||||
_pendingSave = active == null ? null : _QueuedMpvConfig(text: value, revision: revision);
|
||||
_textController.value = TextEditingValue(
|
||||
text: value,
|
||||
selection: TextSelection.collapsed(offset: value.length),
|
||||
);
|
||||
_notifySaveStateChanged();
|
||||
}
|
||||
|
||||
void _queueTextSave(String text) {
|
||||
if (_disposing) return;
|
||||
if (_activeSave == null && _pendingSave == null && text == _persistedText) {
|
||||
_notifySaveStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingSave = _QueuedMpvConfig(text: text, revision: ++_revision);
|
||||
_saveDebouncer.run(() => unawaited(_flushPending()));
|
||||
_notifySaveStateChanged();
|
||||
}
|
||||
|
||||
Future<bool> _flushPending() {
|
||||
_saveDebouncer.cancel();
|
||||
final existing = _drainFuture;
|
||||
if (existing != null) return existing;
|
||||
|
||||
late final Future<bool> drain;
|
||||
drain = _drainPending().whenComplete(() {
|
||||
if (identical(_drainFuture, drain)) {
|
||||
_drainFuture = null;
|
||||
_notifySaveStateChanged();
|
||||
}
|
||||
});
|
||||
_drainFuture = drain;
|
||||
_notifySaveStateChanged();
|
||||
return drain;
|
||||
}
|
||||
|
||||
Future<bool> _drainPending() async {
|
||||
while (true) {
|
||||
final next = _pendingSave;
|
||||
if (next == null) return true;
|
||||
|
||||
_pendingSave = null;
|
||||
_activeSave = next;
|
||||
try {
|
||||
await _settingsService.write(SettingsService.mpvConfigText, next.text);
|
||||
} catch (error, stackTrace) {
|
||||
_pendingSave ??= next;
|
||||
_activeSave = null;
|
||||
appLogger.e('MPV configuration save failed', error: error, stackTrace: stackTrace);
|
||||
if (mounted && !_disposing) showErrorSnackBar(context, t.settings.saveFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
_activeSave = null;
|
||||
if (next.revision >= _persistedRevision) {
|
||||
_persistedRevision = next.revision;
|
||||
_persistedText = next.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _notifySaveStateChanged() {
|
||||
if (mounted && !_disposing) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _flushAndPop() async {
|
||||
if (_isLeaving) return;
|
||||
_isLeaving = true;
|
||||
_notifySaveStateChanged();
|
||||
|
||||
final saved = await _flushPending();
|
||||
if (!mounted || _disposing) return;
|
||||
if (!saved || _hasUnsavedWork) {
|
||||
_isLeaving = false;
|
||||
_notifySaveStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _allowPop = true);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Future<void> _showSavePresetDialog() async {
|
||||
@@ -68,16 +190,21 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
hintText: t.mpvConfig.presetNameHint,
|
||||
);
|
||||
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
if (name == null || name.trim().isEmpty) return;
|
||||
if (!await _flushPending() || !mounted) return;
|
||||
|
||||
await _settingsService.saveMpvPreset(name.trim(), _textController.text);
|
||||
if (mounted) showSuccessSnackBar(context, t.mpvConfig.presetSaved);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadPreset(MpvPreset preset) async {
|
||||
await _settingsService.loadMpvPreset(preset.name);
|
||||
// Controller text is updated reactively via the bindEffect above.
|
||||
if (mounted) showAppSnackBar(context, t.mpvConfig.presetLoaded);
|
||||
_textController.value = TextEditingValue(
|
||||
text: preset.text,
|
||||
selection: TextSelection.collapsed(offset: preset.text.length),
|
||||
);
|
||||
_queueTextSave(preset.text);
|
||||
final saved = await _flushPending();
|
||||
if (mounted && saved) showAppSnackBar(context, t.mpvConfig.presetLoaded);
|
||||
}
|
||||
|
||||
Future<void> _deletePreset(MpvPreset preset) async {
|
||||
@@ -97,15 +224,20 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
listenable: _textFieldFocusNode,
|
||||
builder: (context, _) {
|
||||
return PopScope(
|
||||
canPop: PlatformDetector.isHandheldIOS(context) && !_textFieldFocusNode.hasFocus,
|
||||
canPop:
|
||||
_allowPop ||
|
||||
(PlatformDetector.isHandheldIOS(context) &&
|
||||
!_textFieldFocusNode.hasFocus &&
|
||||
!_hasUnsavedWork &&
|
||||
!_isLeaving),
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
if (didPop || _isLeaving) return;
|
||||
if (BackKeyCoordinator.consumeIfHandled()) return;
|
||||
BackKeyUpSuppressor.suppressBackUntilKeyUp();
|
||||
if (_textFieldFocusNode.hasFocus && _savePresetFocusNode.canRequestFocus) {
|
||||
_savePresetFocusNode.requestFocus();
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
unawaited(_flushAndPop());
|
||||
}
|
||||
},
|
||||
child: FocusedScrollScaffold(
|
||||
@@ -154,11 +286,12 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
final sel = _textController.selection;
|
||||
if (sel.isValid) {
|
||||
final text = _textController.text;
|
||||
_textController.value = TextEditingValue(
|
||||
final value = TextEditingValue(
|
||||
text: text.replaceRange(sel.start, sel.end, '\n'),
|
||||
selection: TextSelection.collapsed(offset: sel.start + 1),
|
||||
);
|
||||
_saveText();
|
||||
_textController.value = value;
|
||||
_queueTextSave(value.text);
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
@@ -187,7 +320,7 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
|
||||
onChanged: (_) => _saveText(),
|
||||
onChanged: _queueTextSave,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -244,3 +377,10 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> with SettingsEffectMi
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QueuedMpvConfig {
|
||||
const _QueuedMpvConfig({required this.text, required this.revision});
|
||||
|
||||
final String text;
|
||||
final int revision;
|
||||
}
|
||||
|
||||
@@ -151,8 +151,8 @@ class _ServiceHubRow extends StatelessWidget {
|
||||
subtitle: Text(username != null ? t.services.connectedAs(username: username!) : t.services.notConnected),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: onTap,
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import '../../providers/trakt_account_provider.dart';
|
||||
import '../../services/keyboard_shortcuts_service.dart';
|
||||
import '../../services/settings_service.dart' as settings;
|
||||
import '../../services/update_service.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
@@ -57,11 +58,22 @@ import 'settings_utils.dart';
|
||||
import '../../widgets/loading_indicator_box.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key, this.downloadDirectoryWritableChecker});
|
||||
const SettingsScreen({
|
||||
super.key,
|
||||
this.downloadDirectoryWritableChecker,
|
||||
this.settingsExporter,
|
||||
this.settingsImporter,
|
||||
});
|
||||
|
||||
@visibleForTesting
|
||||
final Future<bool> Function(Directory directory)? downloadDirectoryWritableChecker;
|
||||
|
||||
@visibleForTesting
|
||||
final Future<String?> Function()? settingsExporter;
|
||||
|
||||
@visibleForTesting
|
||||
final Future<ImportResult?> Function()? settingsImporter;
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
@@ -345,8 +357,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
subtitle: Text(currentPath, maxLines: 2, overflow: .ellipsis),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: () => _showDownloadLocationDialog(),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -542,8 +554,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
_checkForUpdates();
|
||||
}
|
||||
},
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
),
|
||||
_buildAutoCheckUpdatesOnStartupTile(),
|
||||
],
|
||||
@@ -590,8 +602,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||
DialogActionButton(
|
||||
onPressed: () async {
|
||||
await _selectDownloadLocation();
|
||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||
final changed = await _selectDownloadLocation();
|
||||
if (changed && dialogContext.mounted) Navigator.pop(dialogContext);
|
||||
},
|
||||
label: t.settings.selectFolder,
|
||||
isPrimary: true,
|
||||
@@ -601,55 +613,65 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _selectDownloadLocation() async {
|
||||
Future<bool> _selectDownloadLocation() async {
|
||||
try {
|
||||
String? selectedPath;
|
||||
String pathType = 'file';
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final safService = SafStorageService.instance;
|
||||
selectedPath = await safService.pickDirectory();
|
||||
if (selectedPath != null) {
|
||||
pathType = 'saf';
|
||||
} else if (PlatformDetector.isTV()) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, t.settings.downloadLocationSelectError);
|
||||
}
|
||||
return;
|
||||
final safStorage = SafStorageService.instance;
|
||||
if (!safStorage.supportsDirectoryPicker) {
|
||||
showErrorSnackBar(context, t.settings.downloadLocationPickerUnavailable);
|
||||
return false;
|
||||
}
|
||||
selectedPath = await safStorage.pickDirectory();
|
||||
if (!mounted) return false;
|
||||
if (selectedPath != null) pathType = 'saf';
|
||||
} else {
|
||||
final result = await FilePickerService.instance.getDirectoryPath(dialogTitle: t.settings.selectFolder);
|
||||
selectedPath = result;
|
||||
selectedPath = await FilePickerService.instance.getDirectoryPath(dialogTitle: t.settings.selectFolder);
|
||||
if (!mounted) return false;
|
||||
}
|
||||
if (selectedPath == null) return false;
|
||||
|
||||
if (selectedPath != null) {
|
||||
if (pathType == 'file') {
|
||||
final dir = Directory(selectedPath);
|
||||
final isWritable =
|
||||
await (widget.downloadDirectoryWritableChecker ?? DownloadStorageService.instance.isDirectoryWritable)(
|
||||
dir,
|
||||
);
|
||||
await (widget.downloadDirectoryWritableChecker ?? DownloadStorageService.instance.isDirectoryWritable)(dir);
|
||||
if (!mounted) return false;
|
||||
if (!isWritable) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, t.settings.downloadLocationInvalid);
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
await context.read<DownloadProvider>().setDownloadLocation(path: selectedPath, pathType: pathType);
|
||||
if (!mounted) return false;
|
||||
|
||||
if (mounted) {
|
||||
// ignore: no-empty-block - setState triggers rebuild to reflect new download path
|
||||
setState(() {});
|
||||
showSuccessSnackBar(context, t.settings.downloadLocationChanged);
|
||||
return true;
|
||||
} on DownloadStorageException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Download directory selection failed', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Download directory selection', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Download directory selection failed', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, t.settings.downloadLocationSelectError);
|
||||
showSettingsFailure(context, operation: 'Download directory selection', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Download directory selection failed', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Download directory selection', error: error, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,14 +721,27 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
|
||||
Future<void> _handleExportSettings() async {
|
||||
try {
|
||||
final path = await SettingsExportService.exportToFile();
|
||||
if (!mounted) return;
|
||||
if (path == null) return; // user cancelled
|
||||
final path = await (widget.settingsExporter ?? SettingsExportService.exportToFile)();
|
||||
if (!mounted || path == null) return;
|
||||
showSuccessSnackBar(context, t.settings.exportSettingsSuccess);
|
||||
} on SettingsExportException {
|
||||
if (mounted) showErrorSnackBar(context, t.settings.exportSettingsFailed);
|
||||
} catch (_) {
|
||||
if (mounted) showErrorSnackBar(context, t.settings.exportSettingsFailed);
|
||||
} on SettingsExportException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings export failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings export', error: error, stackTrace: stackTrace);
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings export failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings export', error: error, stackTrace: stackTrace);
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings export failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings export', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,22 +752,20 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
message: t.settings.importSettingsConfirm,
|
||||
confirmText: t.settings.importSettings,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (!mounted || !confirmed) return;
|
||||
await _handleImportSettings();
|
||||
}
|
||||
|
||||
Future<void> _handleImportSettings() async {
|
||||
// Capture providers before any awaits so we don't reach through `context`
|
||||
// after the widget may have been unmounted.
|
||||
try {
|
||||
final result = await (widget.settingsImporter ?? SettingsExportService.importFromFile)();
|
||||
if (!mounted) return;
|
||||
if (result == null) return; // user cancelled file picker
|
||||
|
||||
final themeProvider = context.read<ThemeProvider>();
|
||||
final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
|
||||
try {
|
||||
final result = await SettingsExportService.importFromFile();
|
||||
if (!mounted) return;
|
||||
if (result == null) return; // user cancelled file picker
|
||||
|
||||
// Import wrote directly to SharedPreferences, bypassing `write`. Push
|
||||
// fresh values into active listenables before providers re-read settings.
|
||||
_settingsService.refreshListenables();
|
||||
@@ -750,10 +783,24 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
if (mounted) showErrorSnackBar(context, t.settings.importSettingsNoUser);
|
||||
} on InvalidExportFileException {
|
||||
if (mounted) showErrorSnackBar(context, t.settings.importSettingsInvalidFile);
|
||||
} on SettingsExportException {
|
||||
if (mounted) showErrorSnackBar(context, t.settings.importSettingsFailed);
|
||||
} catch (_) {
|
||||
if (mounted) showErrorSnackBar(context, t.settings.importSettingsFailed);
|
||||
} on SettingsExportException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings import failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings import', error: error, stackTrace: stackTrace);
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings import failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings import', error: error, stackTrace: stackTrace);
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
if (!mounted) {
|
||||
appLogger.e('Settings import failed', error: error, stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
showSettingsFailure(context, operation: 'Settings import', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../focus/focusable_text_field.dart';
|
||||
@@ -7,6 +10,8 @@ import '../../focus/input_mode_tracker.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../services/settings_service.dart' as settings;
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/dialog_action_button.dart';
|
||||
import '../../widgets/focusable_list_tile.dart';
|
||||
@@ -39,6 +44,18 @@ typedef _SettingsDialogContentBuilder =
|
||||
|
||||
typedef _SettingsDialogActionsBuilder = List<Widget> Function(BuildContext dialogContext, StateSetter setDialogState);
|
||||
|
||||
/// Reports a recoverable settings persistence failure without swallowing
|
||||
/// programming errors or other unexpected exception types.
|
||||
void showSettingsFailure(
|
||||
BuildContext context, {
|
||||
required String operation,
|
||||
required Object error,
|
||||
required StackTrace stackTrace,
|
||||
}) {
|
||||
appLogger.e('$operation failed', error: error, stackTrace: stackTrace);
|
||||
if (context.mounted) showErrorSnackBar(context, t.settings.saveFailed);
|
||||
}
|
||||
|
||||
void _showSettingsInputDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
@@ -89,8 +106,16 @@ class _SettingsInputDialogState extends State<_SettingsInputDialog> {
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
try {
|
||||
final shouldClose = await widget.onSave(context);
|
||||
if (shouldClose && mounted) Navigator.pop(context);
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
if (!mounted) return;
|
||||
showSettingsFailure(context, operation: 'Settings input save', error: error, stackTrace: stackTrace);
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
if (!mounted) return;
|
||||
showSettingsFailure(context, operation: 'Settings input save', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -347,7 +372,16 @@ Future<void> _showColorInputDialogStandard({
|
||||
},
|
||||
actionButtons: const ColorPickerActionButtons(okButton: true, closeButton: true, dialogActionButtons: false),
|
||||
);
|
||||
if (selected != initial) await onSave(colorToHex(selected));
|
||||
if (selected == initial || !context.mounted) return;
|
||||
try {
|
||||
await onSave(colorToHex(selected));
|
||||
} on PlatformException catch (error, stackTrace) {
|
||||
if (!context.mounted) return;
|
||||
showSettingsFailure(context, operation: 'Color setting save', error: error, stackTrace: stackTrace);
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
if (!context.mounted) return;
|
||||
showSettingsFailure(context, operation: 'Color setting save', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _showColorInputDialogTV({
|
||||
|
||||
@@ -16,7 +16,8 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
|
||||
static KeyboardShortcutsService? _instance;
|
||||
late final SettingsBindingOwner _settingsBinding;
|
||||
Map<String, HotKey> _hotkeys = {};
|
||||
Map<String, HotKey?> _hotkeys = {};
|
||||
Future<void> _shortcutMutationTail = Future.value();
|
||||
int _seekTimeSmall = 10; // Default, loaded from settings
|
||||
int _seekTimeLarge = 30; // Default, loaded from settings
|
||||
bool _settingsInitialized = false;
|
||||
@@ -55,7 +56,7 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
final changed =
|
||||
!_hotkeyMapsEqual(_hotkeys, hotkeys) || _seekTimeSmall != seekTimeSmall || _seekTimeLarge != seekTimeLarge;
|
||||
|
||||
_hotkeys = Map<String, HotKey>.from(hotkeys);
|
||||
_hotkeys = Map<String, HotKey?>.from(hotkeys);
|
||||
_seekTimeSmall = seekTimeSmall;
|
||||
_seekTimeLarge = seekTimeLarge;
|
||||
|
||||
@@ -64,32 +65,49 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
if (notify && changed) notifyListeners();
|
||||
}
|
||||
|
||||
bool _hotkeyMapsEqual(Map<String, HotKey> a, Map<String, HotKey> b) {
|
||||
bool _hotkeyMapsEqual(Map<String, HotKey?> a, Map<String, HotKey?> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (final entry in a.entries) {
|
||||
if (!b.containsKey(entry.key)) return false;
|
||||
final value = entry.value;
|
||||
final other = b[entry.key];
|
||||
if (other == null || !_hotkeyEquals(entry.value, other)) return false;
|
||||
if (value == null || other == null) {
|
||||
if (value != other) return false;
|
||||
} else if (!_hotkeyEquals(value, other)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<String, HotKey> get hotkeys => Map.from(_hotkeys);
|
||||
Map<String, HotKey?> get hotkeys => Map.from(_hotkeys);
|
||||
|
||||
HotKey? getHotkey(String action) {
|
||||
return _hotkeys[action];
|
||||
}
|
||||
|
||||
Future<void> setHotkey(String action, HotKey hotkey) async {
|
||||
await _settingsService.write(SettingsService.keyboardHotkeys, {..._hotkeys, action: hotkey});
|
||||
Future<void> setHotkey(String action, HotKey? hotkey) {
|
||||
return _serializeShortcutMutation(() async {
|
||||
await _settingsService.write(SettingsService.keyboardHotkeys, <String, HotKey?>{..._hotkeys, action: hotkey});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refreshFromStorage() async {
|
||||
_settingsBinding.refresh();
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
final hotkeys = SettingsService.defaultKeyboardHotkeys();
|
||||
await _settingsService.write(SettingsService.keyboardHotkeys, hotkeys);
|
||||
Future<void> resetToDefaults() {
|
||||
return _serializeShortcutMutation(() async {
|
||||
await _settingsService.write(SettingsService.keyboardHotkeys, <String, HotKey?>{
|
||||
...SettingsService.defaultKeyboardHotkeys(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _serializeShortcutMutation(Future<void> Function() operation) {
|
||||
final result = _shortcutMutationTail.then((_) => operation());
|
||||
_shortcutMutationTail = result.then<void>((_) {}, onError: (Object _, StackTrace _) {});
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -175,6 +193,7 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
for (final entry in _hotkeys.entries) {
|
||||
final action = entry.key;
|
||||
final hotkey = entry.value;
|
||||
if (hotkey == null) continue;
|
||||
|
||||
if (physicalKey != hotkey.key) continue;
|
||||
|
||||
@@ -470,7 +489,8 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
// Check if a hotkey is already assigned to another action
|
||||
String? getActionForHotkey(HotKey hotkey) {
|
||||
for (final entry in _hotkeys.entries) {
|
||||
if (_hotkeyEquals(entry.value, hotkey)) {
|
||||
final assignedHotkey = entry.value;
|
||||
if (assignedHotkey != null && _hotkeyEquals(assignedHotkey, hotkey)) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,15 @@ class PlexClient
|
||||
@override
|
||||
ApiCache get cache => _cache;
|
||||
|
||||
/// Snapshot the profile identity used by a cache-first request before its
|
||||
/// cache lookup can yield. [PlexConfig] is immutable, and [headers] returns a
|
||||
/// fresh map, so both the token/client headers and cache namespace stay bound
|
||||
/// to the same profile even if [applyProfileUpdate] runs on a cache miss.
|
||||
({ServerId cacheScope, Map<String, String> headers}) _captureCacheFirstRequestContext() {
|
||||
final requestConfig = config;
|
||||
return (cacheScope: profileScopeId.cacheServerId, headers: Map<String, String>.unmodifiable(requestConfig.headers));
|
||||
}
|
||||
|
||||
/// Whether to operate in offline mode (use cache only)
|
||||
bool _offlineMode = false;
|
||||
|
||||
@@ -886,9 +895,9 @@ class PlexClient
|
||||
Future<bool> isHealthy() async => (await checkHealth()) == HealthStatus.online;
|
||||
|
||||
/// Get running background tasks (thumbnail generation, credit detection, etc.)
|
||||
Future<List<PlexActivity>> getActivities() async {
|
||||
Future<List<PlexActivity>> getActivities({AbortController? abort}) async {
|
||||
try {
|
||||
final response = await _getWithFailover('/activities');
|
||||
final response = await _getWithFailover('/activities', abort: abort);
|
||||
final container = _getMediaContainer(response);
|
||||
if (container == null) return [];
|
||||
final activityList = container['Activity'] as List?;
|
||||
@@ -904,6 +913,10 @@ class PlexClient
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
} on MediaServerHttpException catch (e) {
|
||||
if (e.isCancellation) rethrow;
|
||||
appLogger.e('Failed to get activities', error: e);
|
||||
return [];
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to get activities', error: e);
|
||||
return [];
|
||||
@@ -1557,13 +1570,28 @@ class PlexClient
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
final fetch = forceRefresh ? fetchWithCacheFallback : fetchWithCacheFirst;
|
||||
final data = await fetch<Map<String, dynamic>>(
|
||||
cacheKey: '/library/metadata/$ratingKey',
|
||||
networkCall: () =>
|
||||
_http.get('/library/metadata/$ratingKey', queryParameters: {'includeChapters': 1, 'includeMarkers': 1}),
|
||||
parseCache: (cached) => cached as Map<String, dynamic>?,
|
||||
parseResponse: (response) => response.data as Map<String, dynamic>?,
|
||||
final requestContext = _captureCacheFirstRequestContext();
|
||||
final cacheKey = '/library/metadata/$ratingKey';
|
||||
Future<MediaServerResponse> networkCall() => _http.get(
|
||||
cacheKey,
|
||||
queryParameters: {'includeChapters': 1, 'includeMarkers': 1},
|
||||
headers: requestContext.headers,
|
||||
);
|
||||
Map<String, dynamic>? parseCache(dynamic cached) => cached as Map<String, dynamic>?;
|
||||
Map<String, dynamic>? parseResponse(MediaServerResponse response) => response.data as Map<String, dynamic>?;
|
||||
final data = forceRefresh
|
||||
? await fetchWithCacheFallback<Map<String, dynamic>>(
|
||||
cacheKey: cacheKey,
|
||||
networkCall: networkCall,
|
||||
parseCache: parseCache,
|
||||
parseResponse: parseResponse,
|
||||
)
|
||||
: await fetchWithCacheFirst<Map<String, dynamic>>(
|
||||
cacheScope: requestContext.cacheScope,
|
||||
cacheKey: cacheKey,
|
||||
networkCall: networkCall,
|
||||
parseCache: parseCache,
|
||||
parseResponse: parseResponse,
|
||||
);
|
||||
final metadataJson = _getFirstMetadataJsonFromData(data);
|
||||
return _parsePlaybackExtrasFromMetadataJson(
|
||||
@@ -1696,11 +1724,14 @@ class PlexClient
|
||||
/// for the cache-only readers ([fetchPlaybackExtrasFromCacheOnly],
|
||||
/// [fetchCachedMediaSourceInfo]).
|
||||
Future<Map<String, dynamic>?> _fetchRawMetadataJsonCacheFirst(String ratingKey) async {
|
||||
final requestContext = _captureCacheFirstRequestContext();
|
||||
final data = await fetchWithCacheFirst<Map<String, dynamic>>(
|
||||
cacheScope: requestContext.cacheScope,
|
||||
cacheKey: '/library/metadata/$ratingKey',
|
||||
networkCall: () => _http.get(
|
||||
'/library/metadata/$ratingKey',
|
||||
queryParameters: {'includeChapters': 1, 'includeMarkers': 1, 'checkFiles': 1, 'includeStreams': 1},
|
||||
headers: requestContext.headers,
|
||||
),
|
||||
parseCache: (cached) => cached as Map<String, dynamic>?,
|
||||
parseResponse: (response) => response.data as Map<String, dynamic>?,
|
||||
@@ -3833,10 +3864,18 @@ class PlexClient
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DownloadResolution> resolveDownload(MediaItem item, {int mediaIndex = 0}) async {
|
||||
final playbackData = await getVideoPlaybackData(item.id, mediaIndex: mediaIndex);
|
||||
Future<DownloadResolution> resolveDownload(MediaItem item, {int mediaIndex = 0, String? mediaSourceId}) async {
|
||||
final playbackData = await getVideoPlaybackData(
|
||||
item.id,
|
||||
mediaIndex: mediaIndex,
|
||||
selectedMediaSourceId: mediaSourceId,
|
||||
);
|
||||
final subtitles = <DownloadSubtitleSpec>[];
|
||||
final mediaInfo = playbackData.mediaInfo;
|
||||
final requestedSourceId = mediaSourceId?.trim();
|
||||
if (requestedSourceId != null && requestedSourceId.isNotEmpty && mediaInfo?.mediaSourceId != requestedSourceId) {
|
||||
throw StateError('Requested Plex download source is no longer available');
|
||||
}
|
||||
if (mediaInfo != null) {
|
||||
for (final subtitle in mediaInfo.subtitleTracks) {
|
||||
if (!subtitle.isExternal || subtitle.key == null) continue;
|
||||
|
||||
@@ -4,17 +4,20 @@ import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/shader_preset.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/formatters.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import 'file_picker_service.dart';
|
||||
import 'settings_service.dart';
|
||||
import 'storage_service.dart';
|
||||
import 'trackers/tracker_constants.dart';
|
||||
|
||||
class ImportResult {
|
||||
final int keysImported;
|
||||
@@ -64,8 +67,7 @@ class _StoredPreferenceValue {
|
||||
|
||||
/// Serializes / restores user-facing SharedPreferences to a JSON file.
|
||||
///
|
||||
/// Strategy is allow-by-default: every key is exported unless it matches an
|
||||
/// exact denylist or a prefix denylist of auth/cache/internal keys. User-scoped
|
||||
/// Only preferences in the closed portable registry are exported. User-scoped
|
||||
/// keys (prefixed with `user_{uuid}_`) have that prefix stripped on export and
|
||||
/// re-applied with the current user's prefix on import, so preferences follow
|
||||
/// whichever account is signed in on the target device.
|
||||
@@ -193,12 +195,20 @@ class SettingsExportService {
|
||||
SettingsService.keyboardHotkeys,
|
||||
])
|
||||
pref.key: _PreferencePolicy(_storageTypeFor(pref)),
|
||||
for (final service in TrackerService.values)
|
||||
for (final pref in <Pref<Object?>>[
|
||||
SettingsService.trackerFilterModePref(service),
|
||||
SettingsService.trackerFilterIdsPref(service),
|
||||
])
|
||||
pref.key: _PreferencePolicy(_storageTypeFor(pref)),
|
||||
};
|
||||
|
||||
static const Set<String> _jsonStringListPreferenceKeys = {'hidden_libraries', 'library_order'};
|
||||
|
||||
static const Map<String, _PreferencePolicy> _userScopedPreferences = {
|
||||
'hidden_libraries': _PreferencePolicy(_typeStringList, userScoped: true),
|
||||
'hidden_libraries': _PreferencePolicy(_typeString, userScoped: true),
|
||||
'library_filters': _PreferencePolicy(_typeString, userScoped: true),
|
||||
'library_order': _PreferencePolicy(_typeStringList, userScoped: true),
|
||||
'library_order': _PreferencePolicy(_typeString, userScoped: true),
|
||||
};
|
||||
|
||||
static final List<(RegExp, _PreferencePolicy)> _dynamicUserScopedPreferences = [
|
||||
@@ -269,7 +279,7 @@ class SettingsExportService {
|
||||
final policy = _policyFor(baseKey);
|
||||
if (policy == null || policy.userScoped != sourceIsUserScoped) continue;
|
||||
|
||||
final entry = _encodeValue(prefs.get(fullKey), policy.type);
|
||||
final entry = _encodeValue(_portableExportValue(baseKey, prefs.get(fullKey)), policy.type);
|
||||
if (entry != null) prefsOut[baseKey] = entry;
|
||||
}
|
||||
|
||||
@@ -282,6 +292,11 @@ class SettingsExportService {
|
||||
};
|
||||
}
|
||||
|
||||
static Object? _portableExportValue(String baseKey, Object? value) {
|
||||
if (baseKey != SettingsService.globalShaderPreset.key) return value;
|
||||
return value is String && ShaderPreset.fromId(value) != null ? value : ShaderPreset.none.id;
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _encodeValue(Object? value, String expectedType) {
|
||||
return switch (expectedType) {
|
||||
_typeBool when value is bool => {'type': _typeBool, 'value': value},
|
||||
@@ -331,12 +346,26 @@ class SettingsExportService {
|
||||
continue;
|
||||
}
|
||||
|
||||
final type = rawEntry['type'];
|
||||
final value = rawEntry['value'];
|
||||
var type = rawEntry['type'];
|
||||
var value = rawEntry['value'];
|
||||
// Early format-v1 exports described these JSON-backed values as native
|
||||
// string lists. Normalize that narrowly admitted legacy shape to the
|
||||
// String representation consumed by StorageService.
|
||||
if (version == 1 &&
|
||||
_jsonStringListPreferenceKeys.contains(baseKey) &&
|
||||
type == _typeStringList &&
|
||||
_isValidValue(_typeStringList, value)) {
|
||||
type = _typeString;
|
||||
value = jsonEncode((value as List).cast<String>());
|
||||
}
|
||||
if (type is! String || type != policy.type || !_isValidValue(type, value)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (baseKey == SettingsService.globalShaderPreset.key && value is String && ShaderPreset.fromId(value) == null) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.add(
|
||||
_PendingImport(targetKey: policy.userScoped ? '$userPrefix$baseKey' : baseKey, type: type, value: value),
|
||||
@@ -362,8 +391,7 @@ class SettingsExportService {
|
||||
} catch (rollbackError, rollbackStackTrace) {
|
||||
appLogger.e('Settings import rollback failed', error: rollbackError, stackTrace: rollbackStackTrace);
|
||||
}
|
||||
appLogger.e('Settings import failed', error: error, stackTrace: stackTrace);
|
||||
throw const SettingsExportException('Could not apply settings import');
|
||||
Error.throwWithStackTrace(error, stackTrace);
|
||||
}
|
||||
|
||||
return ImportResult(keysImported: pending.length, keysSkipped: skipped);
|
||||
@@ -436,7 +464,7 @@ class SettingsExportService {
|
||||
/// the user's choosing. Returns the saved path, or `null` if the user
|
||||
/// cancelled the picker.
|
||||
///
|
||||
/// Throws [SettingsExportException] on failure.
|
||||
/// Platform and filesystem failures retain their original exception types.
|
||||
static Future<String?> exportToFile() async {
|
||||
final prefs = (await SettingsService.getInstance()).prefs;
|
||||
final storage = await StorageService.getInstance();
|
||||
@@ -444,8 +472,8 @@ class SettingsExportService {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
appVersion = info.version;
|
||||
} catch (_) {
|
||||
// best-effort; tolerate platforms without PackageInfo
|
||||
} on PlatformException {
|
||||
// Best-effort metadata; platforms without PackageInfo still export.
|
||||
}
|
||||
|
||||
final exportMap = buildExportMap(prefs, currentUserUuid: storage.activeUserScope(), appVersion: appVersion);
|
||||
@@ -459,18 +487,13 @@ class SettingsExportService {
|
||||
return _writeToAppDocuments(fileName, bytes);
|
||||
}
|
||||
|
||||
try {
|
||||
return await FilePickerService.instance.saveFile(
|
||||
return FilePickerService.instance.saveFile(
|
||||
dialogTitle: 'Export Plezy settings',
|
||||
fileName: fileName,
|
||||
bytes: bytes,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: const [fileExtension],
|
||||
);
|
||||
} catch (e, st) {
|
||||
appLogger.e('Settings export failed', error: e, stackTrace: st);
|
||||
throw const SettingsExportException('Could not write export file');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> _writeToAppDocuments(String fileName, Uint8List bytes) async {
|
||||
@@ -483,8 +506,9 @@ class SettingsExportService {
|
||||
/// Prompts the user to pick a settings JSON and writes its contents into
|
||||
/// SharedPreferences. Requires a signed-in user.
|
||||
///
|
||||
/// Returns `null` if the user cancelled. Throws [SettingsExportException] on
|
||||
/// malformed files or unsupported versions.
|
||||
/// Returns `null` if the user cancelled. Malformed files throw
|
||||
/// [InvalidExportFileException]; platform and filesystem failures retain
|
||||
/// their original exception types.
|
||||
static Future<ImportResult?> importFromFile() async {
|
||||
final storage = await StorageService.getInstance();
|
||||
final uuid = storage.activeUserScope();
|
||||
@@ -501,30 +525,29 @@ class SettingsExportService {
|
||||
|
||||
final file = picked.files.first;
|
||||
String contents;
|
||||
try {
|
||||
final bytes = file.bytes;
|
||||
if (bytes != null) {
|
||||
try {
|
||||
contents = utf8.decode(bytes);
|
||||
} on FormatException {
|
||||
throw const InvalidExportFileException('Could not read the selected file');
|
||||
}
|
||||
} else if (file.path != null) {
|
||||
contents = await File(file.path!).readAsString();
|
||||
} else {
|
||||
throw const InvalidExportFileException('Could not read the selected file');
|
||||
}
|
||||
} catch (e, st) {
|
||||
appLogger.e('Settings import read failed', error: e, stackTrace: st);
|
||||
throw const InvalidExportFileException('Could not read the selected file');
|
||||
}
|
||||
|
||||
Map<String, dynamic> data;
|
||||
final Object? decoded;
|
||||
try {
|
||||
final decoded = json.decode(contents);
|
||||
decoded = json.decode(contents);
|
||||
} on FormatException {
|
||||
throw const InvalidExportFileException('Invalid export file');
|
||||
}
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const InvalidExportFileException('Invalid export file');
|
||||
}
|
||||
data = decoded;
|
||||
} catch (_) {
|
||||
throw const InvalidExportFileException('Invalid export file');
|
||||
}
|
||||
final data = decoded;
|
||||
|
||||
final prefs = (await SettingsService.getInstance()).prefs;
|
||||
return applyImportMap(data, prefs, currentUserUuid: uuid);
|
||||
|
||||
@@ -304,13 +304,19 @@ Map<String, HotKey> _defaultKeyboardHotkeys() => {
|
||||
'screenshot': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.control]),
|
||||
};
|
||||
|
||||
Map<String, HotKey> _decodeKeyboardHotkeys(dynamic raw) {
|
||||
final result = <String, HotKey>{};
|
||||
Map<String, HotKey?> _decodeKeyboardHotkeys(dynamic raw) {
|
||||
final result = <String, HotKey?>{};
|
||||
for (final entry in (raw as Map<String, dynamic>).entries) {
|
||||
final hk = SettingsService.deserializeHotKey(entry.value as Map<String, dynamic>);
|
||||
if (hk != null) result[entry.key] = hk;
|
||||
final value = entry.value;
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
if (value['disabled'] == true) {
|
||||
result[entry.key] = null;
|
||||
continue;
|
||||
}
|
||||
return {..._defaultKeyboardHotkeys(), ...result};
|
||||
final hotkey = SettingsService.deserializeHotKey(value);
|
||||
if (hotkey != null) result[entry.key] = hotkey;
|
||||
}
|
||||
return <String, HotKey?>{..._defaultKeyboardHotkeys(), ...result};
|
||||
}
|
||||
|
||||
class SettingsService extends BaseSharedPreferencesService {
|
||||
@@ -500,10 +506,12 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
);
|
||||
static const mpvConfigText = _MpvConfigTextPref();
|
||||
|
||||
static final keyboardHotkeys = JsonPref<Map<String, HotKey>>(
|
||||
static final keyboardHotkeys = JsonPref<Map<String, HotKey?>>(
|
||||
'keyboard_hotkeys',
|
||||
defaultValue: _defaultKeyboardHotkeys(),
|
||||
encode: (v) => json.encode(v.map((k, hk) => MapEntry(k, SettingsService.serializeHotKey(hk)))),
|
||||
defaultValue: <String, HotKey?>{..._defaultKeyboardHotkeys()},
|
||||
encode: (values) => json.encode(
|
||||
values.map((key, hotkey) => MapEntry(key, hotkey == null ? const {'disabled': true} : serializeHotKey(hotkey))),
|
||||
),
|
||||
decode: _decodeKeyboardHotkeys,
|
||||
);
|
||||
static final mediaVersionPreferences = JsonPref<Map<String, MediaVersionPreference>>(
|
||||
@@ -672,13 +680,6 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
await write(mpvPresets, presets);
|
||||
}
|
||||
|
||||
/// Load a preset (replaces current config text).
|
||||
Future<void> loadMpvPreset(String name) async {
|
||||
final presets = read(mpvPresets);
|
||||
final preset = presets.firstWhere((p) => p.name == name, orElse: () => throw Exception('Preset not found: $name'));
|
||||
await write(mpvConfigText, preset.text);
|
||||
}
|
||||
|
||||
static const _modifierMap = <String, HotKeyModifier>{
|
||||
'alt': HotKeyModifier.alt,
|
||||
'control': HotKeyModifier.control,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async' show unawaited;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -15,6 +17,10 @@ import '../utils/app_logger.dart';
|
||||
class ShaderAssetLoader {
|
||||
static const String _shaderAssetBase = 'assets/shaders';
|
||||
static String? _cachedShaderDir;
|
||||
static final RegExp _customShaderFileNamePattern = RegExp(r'^[A-Za-z0-9-]+\.glsl$', caseSensitive: false);
|
||||
static final Map<String, String> _verifiedBuiltInShaderPaths = {};
|
||||
static final Map<String, Future<String?>> _inFlightBuiltInShaders = {};
|
||||
static int _cacheGeneration = 0;
|
||||
|
||||
/// NVScaler shader file
|
||||
static const String _nvscalerShader = 'nvscaler/NVScaler.glsl';
|
||||
@@ -42,13 +48,12 @@ class ShaderAssetLoader {
|
||||
'downscale_post': 'anime4k/Anime4K_AutoDownscalePre_x4.glsl',
|
||||
};
|
||||
|
||||
/// Get the shader cache directory path, creating it if necessary.
|
||||
/// Get the application-owned shader cache directory, creating it if needed.
|
||||
static Future<String> _getShaderDirectory() async {
|
||||
if (_cachedShaderDir != null) return _cachedShaderDir!;
|
||||
|
||||
final cacheDir = await getTemporaryDirectory();
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
final shaderDir = Directory(path.join(cacheDir.path, 'shaders'));
|
||||
|
||||
if (!await shaderDir.exists()) {
|
||||
await shaderDir.create(recursive: true);
|
||||
}
|
||||
@@ -59,30 +64,102 @@ class ShaderAssetLoader {
|
||||
|
||||
/// Extract a single shader file from assets to the cache directory.
|
||||
/// Returns the absolute file path of the extracted shader.
|
||||
static Future<String?> _extractShader(String assetPath) async {
|
||||
try {
|
||||
final shaderDir = await _getShaderDirectory();
|
||||
final fileName = path.basename(assetPath);
|
||||
final subDir = path.dirname(assetPath);
|
||||
static Future<String?> _extractShader(String assetPath) {
|
||||
final generation = _cacheGeneration;
|
||||
final operationKey = '$generation:$assetPath';
|
||||
final active = _inFlightBuiltInShaders[operationKey];
|
||||
if (active != null) return active;
|
||||
|
||||
final targetDir = Directory(path.join(shaderDir, subDir));
|
||||
final operation = _materializeBuiltInShader(assetPath, generation);
|
||||
_inFlightBuiltInShaders[operationKey] = operation;
|
||||
unawaited(
|
||||
operation.whenComplete(() {
|
||||
if (identical(_inFlightBuiltInShaders[operationKey], operation)) {
|
||||
_inFlightBuiltInShaders.remove(operationKey);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return operation;
|
||||
}
|
||||
|
||||
static Future<String?> _materializeBuiltInShader(String assetPath, int generation) async {
|
||||
File? pendingFile;
|
||||
try {
|
||||
final verifiedPath = _verifiedBuiltInShaderPaths[assetPath];
|
||||
if (verifiedPath != null) {
|
||||
if (await File(verifiedPath).exists()) return verifiedPath;
|
||||
_verifiedBuiltInShaderPaths.remove(assetPath);
|
||||
}
|
||||
|
||||
final shaderDir = await _getShaderDirectory();
|
||||
final targetDir = Directory(path.join(shaderDir, path.dirname(assetPath)));
|
||||
if (!await targetDir.exists()) {
|
||||
await targetDir.create(recursive: true);
|
||||
}
|
||||
final targetFile = File(path.join(targetDir.path, path.basename(assetPath)));
|
||||
final data = await rootBundle.load('$_shaderAssetBase/$assetPath');
|
||||
final bundledBytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
|
||||
final targetFile = File(path.join(targetDir.path, fileName));
|
||||
|
||||
// Only extract if not already cached
|
||||
if (!await targetFile.exists()) {
|
||||
final fullAssetPath = '$_shaderAssetBase/$assetPath';
|
||||
final data = await rootBundle.load(fullAssetPath);
|
||||
await targetFile.writeAsBytes(data.buffer.asUint8List());
|
||||
if (await _fileMatches(targetFile, bundledBytes)) {
|
||||
if (generation == _cacheGeneration) {
|
||||
_verifiedBuiltInShaderPaths[assetPath] = targetFile.path;
|
||||
}
|
||||
return targetFile.path;
|
||||
}
|
||||
|
||||
pendingFile = File('${targetFile.path}.pending.${const Uuid().v4()}');
|
||||
await pendingFile.writeAsBytes(bundledBytes, flush: true);
|
||||
if (!await _promotePendingShader(pendingFile, targetFile, bundledBytes)) {
|
||||
return null;
|
||||
}
|
||||
if (!await _fileMatches(targetFile, bundledBytes)) return null;
|
||||
|
||||
if (generation == _cacheGeneration) {
|
||||
_verifiedBuiltInShaderPaths[assetPath] = targetFile.path;
|
||||
}
|
||||
return targetFile.path;
|
||||
} catch (e, st) {
|
||||
appLogger.w('Failed to extract shader $assetPath', error: e, stackTrace: st);
|
||||
return null;
|
||||
} finally {
|
||||
if (pendingFile != null) {
|
||||
try {
|
||||
if (await pendingFile.exists()) await pendingFile.delete();
|
||||
} on FileSystemException {
|
||||
// The pending path is never returned and can be reclaimed with cache.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _promotePendingShader(File pendingFile, File targetFile, List<int> bundledBytes) async {
|
||||
try {
|
||||
await pendingFile.rename(targetFile.path);
|
||||
return true;
|
||||
} on FileSystemException {
|
||||
if (await _fileMatches(targetFile, bundledBytes)) return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await targetFile.exists()) await targetFile.delete();
|
||||
await pendingFile.rename(targetFile.path);
|
||||
return true;
|
||||
} on FileSystemException {
|
||||
return _fileMatches(targetFile, bundledBytes);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _fileMatches(File file, List<int> expected) async {
|
||||
try {
|
||||
if (!await file.exists()) return false;
|
||||
final actual = await file.readAsBytes();
|
||||
if (actual.length != expected.length) return false;
|
||||
for (var index = 0; index < expected.length; index++) {
|
||||
if (actual[index] != expected[index]) return false;
|
||||
}
|
||||
return true;
|
||||
} on FileSystemException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +271,14 @@ class ShaderAssetLoader {
|
||||
return shaders;
|
||||
}
|
||||
|
||||
/// Whether [fileName] is a direct managed GLSL shader basename.
|
||||
///
|
||||
/// UUID names generated by current builds and alphanumeric names generated
|
||||
/// by older builds are both accepted.
|
||||
static bool isValidCustomShaderFileName(String fileName) {
|
||||
return _customShaderFileNamePattern.hasMatch(fileName);
|
||||
}
|
||||
|
||||
/// Get the custom shader directory path, creating it if necessary.
|
||||
/// Uses app support directory (persistent) rather than temp/cache.
|
||||
static Future<String> _getCustomShaderDirectory() async {
|
||||
@@ -210,29 +295,31 @@ class ShaderAssetLoader {
|
||||
/// Import a custom shader file into the custom shaders directory.
|
||||
/// Returns the stored file name (UUID-based to avoid collisions).
|
||||
static Future<String> importCustomShader(String sourcePath) async {
|
||||
final customDir = await _getCustomShaderDirectory();
|
||||
final ext = path.extension(sourcePath);
|
||||
final uuid = const Uuid().v4();
|
||||
final storedName = '$uuid$ext';
|
||||
final targetFile = File(path.join(customDir, storedName));
|
||||
if (path.extension(sourcePath).toLowerCase() != '.glsl') {
|
||||
throw ArgumentError.value(sourcePath, 'sourcePath', 'Custom shaders must use the .glsl extension');
|
||||
}
|
||||
|
||||
await File(sourcePath).copy(targetFile.path);
|
||||
final customDir = await _getCustomShaderDirectory();
|
||||
final storedName = '${const Uuid().v4()}.glsl';
|
||||
await File(sourcePath).copy(path.join(customDir, storedName));
|
||||
return storedName;
|
||||
}
|
||||
|
||||
/// Delete a custom shader file from the custom shaders directory.
|
||||
static Future<void> deleteCustomShader(String fileName) async {
|
||||
final customDir = await _getCustomShaderDirectory();
|
||||
final file = File(path.join(customDir, fileName));
|
||||
if (await file.exists()) {
|
||||
final file = await _resolveManagedCustomShaderFile(fileName);
|
||||
if (file != null && await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the absolute path for a custom shader file.
|
||||
static Future<String> getCustomShaderPath(String fileName) async {
|
||||
final customDir = await _getCustomShaderDirectory();
|
||||
return path.join(customDir, fileName);
|
||||
static Future<File?> _resolveManagedCustomShaderFile(String fileName) async {
|
||||
if (!isValidCustomShaderFileName(fileName)) return null;
|
||||
|
||||
final customDir = path.canonicalize(await _getCustomShaderDirectory());
|
||||
final candidate = path.canonicalize(path.join(customDir, fileName));
|
||||
if (!path.equals(path.dirname(candidate), customDir)) return null;
|
||||
return File(candidate);
|
||||
}
|
||||
|
||||
/// Get shader paths for a given preset.
|
||||
@@ -250,16 +337,19 @@ class ShaderAssetLoader {
|
||||
if (preset.anime4kConfig == null) return [];
|
||||
return getAnime4KShaders(preset.anime4kConfig!);
|
||||
case ShaderPresetType.custom:
|
||||
if (preset.fileName == null) return [];
|
||||
final shaderPath = await getCustomShaderPath(preset.fileName!);
|
||||
if (!await File(shaderPath).exists()) return [];
|
||||
return [shaderPath];
|
||||
final fileName = preset.fileName;
|
||||
if (fileName == null) return [];
|
||||
final shaderFile = await _resolveManagedCustomShaderFile(fileName);
|
||||
if (shaderFile == null || !await shaderFile.exists()) return [];
|
||||
return [shaderFile.path];
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear cached shader directory reference.
|
||||
/// Call when clearing app cache.
|
||||
static void clearCache() {
|
||||
_cacheGeneration++;
|
||||
_cachedShaderDir = null;
|
||||
_verifiedBuiltInShaderPaths.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
|
||||
String initialOf(String name) {
|
||||
final trimmed = name.trim();
|
||||
if (trimmed.isEmpty) return '?';
|
||||
final first = trimmed.runes.first;
|
||||
return String.fromCharCode(first).toUpperCase();
|
||||
return trimmed.characters.first.toUpperCase();
|
||||
}
|
||||
|
||||
/// Deterministic colour for [name] from a curated palette. The palette is
|
||||
|
||||
@@ -1,85 +1,98 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:string_similarity/string_similarity.dart';
|
||||
import 'package:unorm_dart/unorm_dart.dart';
|
||||
|
||||
import '../media/media_item.dart';
|
||||
|
||||
const int defaultMediaSearchLimit = 100;
|
||||
|
||||
final RegExp _searchSeparatorPattern = RegExp(r'[^\p{L}\p{N}\p{M}]+', unicode: true);
|
||||
|
||||
List<MediaItem> rankMediaSearchResults(List<MediaItem> items, String query, {int? limit}) {
|
||||
final normalizedQuery = normalizeSearchText(query);
|
||||
if (normalizedQuery.isEmpty) {
|
||||
if (limit != null) {
|
||||
RangeError.checkNotNegative(limit, 'limit');
|
||||
if (limit == 0) return const [];
|
||||
}
|
||||
if (items.isEmpty) return const [];
|
||||
|
||||
final searchQuery = _NormalizedSearchQuery(query);
|
||||
if (searchQuery.text.isEmpty) {
|
||||
return limit == null ? List<MediaItem>.of(items) : items.take(limit).toList();
|
||||
}
|
||||
|
||||
if (limit == null || limit >= items.length) {
|
||||
final ranked = <_RankedMediaItem>[
|
||||
for (var i = 0; i < items.length; i++)
|
||||
_RankedMediaItem(item: items[i], score: mediaSearchRelevanceScore(items[i], normalizedQuery), originalIndex: i),
|
||||
];
|
||||
_RankedMediaItem(
|
||||
item: items[i],
|
||||
score: _mediaSearchRelevanceScoreNormalized(items[i], searchQuery),
|
||||
originalIndex: i,
|
||||
),
|
||||
]..sort(_compareRankedBestFirst);
|
||||
return [for (final entry in ranked) entry.item];
|
||||
}
|
||||
|
||||
ranked.sort((a, b) {
|
||||
final scoreComparison = b.score.compareTo(a.score);
|
||||
if (scoreComparison != 0) return scoreComparison;
|
||||
return a.originalIndex.compareTo(b.originalIndex);
|
||||
});
|
||||
final retained = HeapPriorityQueue<_RankedMediaItem>(_compareRankedWorstFirst);
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final item = items[i];
|
||||
final score = _mediaSearchRelevanceScoreNormalized(item, searchQuery);
|
||||
if (retained.length < limit) {
|
||||
retained.add(_RankedMediaItem(item: item, score: score, originalIndex: i));
|
||||
continue;
|
||||
}
|
||||
|
||||
final result = ranked.map((entry) => entry.item);
|
||||
return limit == null ? result.toList() : result.take(limit).toList();
|
||||
final worst = retained.first;
|
||||
if (score > worst.score || (score == worst.score && i < worst.originalIndex)) {
|
||||
retained
|
||||
..removeFirst()
|
||||
..add(_RankedMediaItem(item: item, score: score, originalIndex: i));
|
||||
}
|
||||
}
|
||||
|
||||
final ranked = retained.toList()..sort(_compareRankedBestFirst);
|
||||
return [for (final entry in ranked) entry.item];
|
||||
}
|
||||
|
||||
double mediaSearchRelevanceScore(MediaItem item, String query) {
|
||||
final normalizedQuery = normalizeSearchText(query);
|
||||
if (normalizedQuery.isEmpty) return 0;
|
||||
final fields = <({String? value, double weight})>[
|
||||
(value: item.title, weight: 1.0),
|
||||
(value: item.titleSort, weight: 0.98),
|
||||
(value: item.originalTitle, weight: 0.96),
|
||||
(value: item.grandparentTitle, weight: 0.9),
|
||||
(value: item.parentTitle, weight: 0.8),
|
||||
];
|
||||
|
||||
var best = 0.0;
|
||||
for (final field in fields) {
|
||||
final candidate = normalizeSearchText(field.value);
|
||||
if (candidate.isEmpty) continue;
|
||||
best = math.max(best, _scoreNormalizedField(normalizedQuery, candidate) * field.weight);
|
||||
}
|
||||
double _mediaSearchRelevanceScoreNormalized(MediaItem item, _NormalizedSearchQuery query) {
|
||||
var best = _scoreWeightedField(item.title, query, 1.0);
|
||||
best = math.max(best, _scoreWeightedField(item.titleSort, query, 0.98));
|
||||
best = math.max(best, _scoreWeightedField(item.originalTitle, query, 0.96));
|
||||
best = math.max(best, _scoreWeightedField(item.grandparentTitle, query, 0.9));
|
||||
best = math.max(best, _scoreWeightedField(item.parentTitle, query, 0.8));
|
||||
return best;
|
||||
}
|
||||
|
||||
String normalizeSearchText(String? value) {
|
||||
if (value == null) return '';
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(r'[\u0000-\u002f\u003a-\u0040\u005b-\u0060\u007b-\u007f]+'), ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
double _scoreWeightedField(String? value, _NormalizedSearchQuery query, double weight) {
|
||||
final candidate = normalizeSearchText(value);
|
||||
if (candidate.isEmpty) return 0;
|
||||
return _scoreNormalizedField(query, candidate) * weight;
|
||||
}
|
||||
|
||||
double _scoreNormalizedField(String query, String candidate) {
|
||||
if (candidate == query) return 1000;
|
||||
/// Produces an accent-sensitive search key where canonical/compatibility
|
||||
/// equivalents and Unicode typography compare alike.
|
||||
String normalizeSearchText(String? value) {
|
||||
if (value == null) return '';
|
||||
return nfkc(value).toLowerCase().replaceAll(_searchSeparatorPattern, ' ').trim();
|
||||
}
|
||||
|
||||
final queryWithoutArticle = _withoutLeadingArticle(query);
|
||||
final candidateWithoutArticle = _withoutLeadingArticle(candidate);
|
||||
if (queryWithoutArticle.isNotEmpty && candidateWithoutArticle == queryWithoutArticle) return 980;
|
||||
double _scoreNormalizedField(_NormalizedSearchQuery query, String candidate) {
|
||||
if (candidate == query.text) return 1000;
|
||||
|
||||
if (candidate.startsWith(query)) return 900 + _lengthCloseness(query, candidate, 50);
|
||||
if (queryWithoutArticle.isNotEmpty && candidateWithoutArticle.startsWith(queryWithoutArticle)) {
|
||||
return 880 + _lengthCloseness(queryWithoutArticle, candidateWithoutArticle, 50);
|
||||
}
|
||||
if (candidate.startsWith(query.text)) return 900 + _lengthCloseness(query.text, candidate, 50);
|
||||
|
||||
if (candidate.contains(query)) return 800 + _lengthCloseness(query, candidate, 50);
|
||||
if (candidate.contains(query.text)) return 800 + _lengthCloseness(query.text, candidate, 50);
|
||||
|
||||
final queryTokens = _tokens(query);
|
||||
final queryTokens = query.tokens;
|
||||
final candidateTokens = _tokens(candidate);
|
||||
if (queryTokens.isEmpty || candidateTokens.isEmpty) return 0;
|
||||
|
||||
final candidateTokenSet = candidateTokens.toSet();
|
||||
final matchingTokens = queryTokens.where(candidateTokenSet.contains).length;
|
||||
final sortedQuery = _sortedTokens(queryTokens);
|
||||
final sortedCandidate = _sortedTokens(candidateTokens);
|
||||
final tokenSimilarity = StringSimilarity.compareTwoStrings(sortedQuery, sortedCandidate);
|
||||
final rawSimilarity = StringSimilarity.compareTwoStrings(query, candidate);
|
||||
final tokenSimilarity = StringSimilarity.compareTwoStrings(query.sortedTokens, sortedCandidate);
|
||||
final rawSimilarity = StringSimilarity.compareTwoStrings(query.text, candidate);
|
||||
final fuzzyScore = math.max(rawSimilarity, tokenSimilarity) * 650;
|
||||
|
||||
if (matchingTokens == queryTokens.length) return math.max(700 + tokenSimilarity * 100, fuzzyScore);
|
||||
@@ -95,13 +108,6 @@ String _sortedTokens(List<String> tokens) {
|
||||
return sorted.join(' ');
|
||||
}
|
||||
|
||||
String _withoutLeadingArticle(String value) {
|
||||
for (final article in const ['the ', 'a ', 'an ']) {
|
||||
if (value.startsWith(article)) return value.substring(article.length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double _lengthCloseness(String query, String candidate, double maxBonus) {
|
||||
final longest = math.max(query.length, candidate.length);
|
||||
if (longest == 0) return 0;
|
||||
@@ -110,6 +116,26 @@ double _lengthCloseness(String query, String candidate, double maxBonus) {
|
||||
return maxBonus * closeness;
|
||||
}
|
||||
|
||||
int _compareRankedBestFirst(_RankedMediaItem a, _RankedMediaItem b) {
|
||||
final scoreComparison = b.score.compareTo(a.score);
|
||||
if (scoreComparison != 0) return scoreComparison;
|
||||
return a.originalIndex.compareTo(b.originalIndex);
|
||||
}
|
||||
|
||||
int _compareRankedWorstFirst(_RankedMediaItem a, _RankedMediaItem b) {
|
||||
final scoreComparison = a.score.compareTo(b.score);
|
||||
if (scoreComparison != 0) return scoreComparison;
|
||||
return b.originalIndex.compareTo(a.originalIndex);
|
||||
}
|
||||
|
||||
class _NormalizedSearchQuery {
|
||||
_NormalizedSearchQuery(String value) : text = normalizeSearchText(value);
|
||||
|
||||
final String text;
|
||||
late final List<String> tokens = _tokens(text);
|
||||
late final String sortedTokens = _sortedTokens(tokens);
|
||||
}
|
||||
|
||||
class _RankedMediaItem {
|
||||
const _RankedMediaItem({required this.item, required this.score, required this.originalIndex});
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../focus/focusable_wrapper.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'app_icon.dart';
|
||||
|
||||
/// Defines the visual style of the back button
|
||||
enum BackButtonStyle {
|
||||
/// Back button with circular semi-transparent background (used in detail screens)
|
||||
@@ -31,13 +35,15 @@ class AppBarBackButton extends StatefulWidget {
|
||||
/// [style] determines the visual appearance of the back button.
|
||||
/// [onPressed] is called when the button is tapped. If null, defaults to Navigator.pop.
|
||||
/// [color] overrides the default icon color. If null, uses white for circular/video, theme default for plain.
|
||||
/// [semanticLabel] provides accessibility label for screen readers.
|
||||
/// [focusNode] allows callers to connect this control to an explicit focus graph.
|
||||
/// [semanticLabel] overrides the localized back-button label.
|
||||
const AppBarBackButton({
|
||||
super.key,
|
||||
this.style = BackButtonStyle.circular,
|
||||
this.onPressed,
|
||||
this.color,
|
||||
this.semanticLabel,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
final BackButtonStyle style;
|
||||
@@ -49,6 +55,7 @@ class AppBarBackButton extends StatefulWidget {
|
||||
final Color? color;
|
||||
|
||||
final String? semanticLabel;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
@override
|
||||
State<AppBarBackButton> createState() => _AppBarBackButtonState();
|
||||
@@ -90,6 +97,12 @@ class _AppBarBackButtonState extends State<AppBarBackButton> with TickerProvider
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.space) return KeyEventResult.ignored;
|
||||
if (event is KeyDownEvent) _handlePressed();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -123,11 +136,24 @@ class _AppBarBackButtonState extends State<AppBarBackButton> with TickerProvider
|
||||
break;
|
||||
}
|
||||
|
||||
final buttonWidget = MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
final semanticLabel = widget.semanticLabel ?? t.common.back;
|
||||
final button = FocusableWrapper(
|
||||
focusNode: widget.focusNode,
|
||||
semanticLabel: semanticLabel,
|
||||
onSelect: _handlePressed,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
autoScroll: false,
|
||||
disableScale: true,
|
||||
descendantsAreFocusable: false,
|
||||
borderRadius: 20,
|
||||
child: Tooltip(
|
||||
message: semanticLabel,
|
||||
excludeFromSemantics: true,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => _onHoverChange(true),
|
||||
onExit: (_) => _onHoverChange(false),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: _handlePressed,
|
||||
child: AnimatedBuilder(
|
||||
animation: _backgroundAnimation,
|
||||
@@ -144,12 +170,10 @@ class _AppBarBackButtonState extends State<AppBarBackButton> with TickerProvider
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final button = widget.semanticLabel != null
|
||||
? Semantics(label: widget.semanticLabel, button: true, excludeSemantics: true, child: buttonWidget)
|
||||
: buttonWidget;
|
||||
|
||||
return widget.style == BackButtonStyle.circular ? SafeArea(child: button) : button;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import 'package:flutter/material.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../services/device_performance.dart';
|
||||
import '../utils/media_image_helper.dart';
|
||||
import 'optimized_media_image.dart';
|
||||
|
||||
Future<bool> _defaultLocalFileExists(File file) => file.exists();
|
||||
|
||||
/// Displays server artwork and rotates through multiple backdrops in order.
|
||||
///
|
||||
@@ -25,6 +28,7 @@ class CyclingMediaBackdrop extends StatefulWidget {
|
||||
this.localArtworkPathResolver,
|
||||
this.imageProviderResolver,
|
||||
this.allowNetwork = true,
|
||||
this.localFileExists = _defaultLocalFileExists,
|
||||
this.active = true,
|
||||
this.fit = BoxFit.cover,
|
||||
this.alignment = Alignment.center,
|
||||
@@ -41,6 +45,10 @@ class CyclingMediaBackdrop extends StatefulWidget {
|
||||
/// Overrides provider construction for deterministic widget tests.
|
||||
@visibleForTesting
|
||||
final ImageProvider? Function(String artworkPath)? imageProviderResolver;
|
||||
|
||||
/// Overrides local file checks for deterministic widget tests.
|
||||
@visibleForTesting
|
||||
final Future<bool> Function(File file) localFileExists;
|
||||
final bool allowNetwork;
|
||||
final bool active;
|
||||
final double width;
|
||||
@@ -220,7 +228,7 @@ class _CyclingMediaBackdropState extends State<CyclingMediaBackdrop> with Widget
|
||||
_restartRotationTimer();
|
||||
}
|
||||
|
||||
ImageProvider? _providerFor(BuildContext context, String path) {
|
||||
ImageProvider? _providerFor(BuildContext context, String path, File? localFile) {
|
||||
final providerOverride = widget.imageProviderResolver;
|
||||
if (providerOverride != null) return providerOverride(path);
|
||||
|
||||
@@ -234,12 +242,8 @@ class _CyclingMediaBackdropState extends State<CyclingMediaBackdrop> with Widget
|
||||
imageType: ImageType.art,
|
||||
);
|
||||
|
||||
final localPath = widget.localArtworkPathResolver?.call(path);
|
||||
if (localPath != null) {
|
||||
final file = File(localPath);
|
||||
if (file.existsSync()) {
|
||||
return MediaImageHelper.boundedDecode(FileImage(file), memWidth: memWidth, memHeight: memHeight);
|
||||
}
|
||||
if (localFile != null) {
|
||||
return MediaImageHelper.boundedDecode(FileImage(localFile), memWidth: memWidth, memHeight: memHeight);
|
||||
}
|
||||
if (!widget.allowNetwork) return null;
|
||||
|
||||
@@ -262,17 +266,22 @@ class _CyclingMediaBackdropState extends State<CyclingMediaBackdrop> with Widget
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final path = _currentPath;
|
||||
final provider = path == null ? null : _providerFor(context, path);
|
||||
if (path != null && provider == null) _reportMissingProvider(path);
|
||||
Widget _buildCrossfade(
|
||||
BuildContext context,
|
||||
String? path, {
|
||||
required LocalFileResolution localResolution,
|
||||
File? localFile,
|
||||
}) {
|
||||
final pending = localResolution == LocalFileResolution.pending;
|
||||
final provider = path == null || pending ? null : _providerFor(context, path, localFile);
|
||||
if (path != null && provider == null && !pending) _reportMissingProvider(path);
|
||||
final fadeDuration = _disableAnimations ? Duration.zero : DevicePerformance.reducedDuration(widget.fadeDuration);
|
||||
|
||||
return _BackdropArtworkCrossfade(
|
||||
artworkKey: (widget.mediaKey, path),
|
||||
imageErrorKey: path,
|
||||
image: provider,
|
||||
pending: pending,
|
||||
duration: fadeDuration,
|
||||
fit: widget.fit,
|
||||
alignment: widget.alignment,
|
||||
@@ -280,6 +289,27 @@ class _CyclingMediaBackdropState extends State<CyclingMediaBackdrop> with Widget
|
||||
onImageError: _handleImageError,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final path = _currentPath;
|
||||
if (path == null) {
|
||||
return _buildCrossfade(context, null, localResolution: LocalFileResolution.missing);
|
||||
}
|
||||
|
||||
final localPath = widget.localArtworkPathResolver?.call(path);
|
||||
if (localPath == null) {
|
||||
return _buildCrossfade(context, path, localResolution: LocalFileResolution.missing);
|
||||
}
|
||||
|
||||
return ResolvedLocalFile(
|
||||
path: localPath,
|
||||
cacheMissing: true,
|
||||
fileExists: widget.localFileExists,
|
||||
builder: (context, resolution, file) =>
|
||||
_buildCrossfade(context, path, localResolution: resolution, localFile: file),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackdropArtworkCrossfade extends StatefulWidget {
|
||||
@@ -287,6 +317,7 @@ class _BackdropArtworkCrossfade extends StatefulWidget {
|
||||
required this.artworkKey,
|
||||
required this.imageErrorKey,
|
||||
required this.image,
|
||||
required this.pending,
|
||||
required this.duration,
|
||||
required this.fit,
|
||||
required this.alignment,
|
||||
@@ -297,6 +328,7 @@ class _BackdropArtworkCrossfade extends StatefulWidget {
|
||||
final Object? artworkKey;
|
||||
final Object? imageErrorKey;
|
||||
final ImageProvider? image;
|
||||
final bool pending;
|
||||
final Duration duration;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
@@ -310,7 +342,7 @@ class _BackdropArtworkCrossfade extends StatefulWidget {
|
||||
class _BackdropArtworkCrossfadeState extends State<_BackdropArtworkCrossfade> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _fade;
|
||||
late Object? _currentKey = widget.artworkKey;
|
||||
late ImageProvider? _base = widget.image;
|
||||
late ImageProvider? _base = widget.pending ? null : widget.image;
|
||||
late Object? _baseErrorKey = widget.imageErrorKey;
|
||||
ImageProvider? _incoming;
|
||||
Object? _incomingErrorKey;
|
||||
@@ -328,6 +360,11 @@ class _BackdropArtworkCrossfadeState extends State<_BackdropArtworkCrossfade> wi
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_fade.duration = widget.duration;
|
||||
if (widget.artworkKey == _currentKey) {
|
||||
if (widget.pending) return;
|
||||
if (oldWidget.pending) {
|
||||
_transitionToIncoming();
|
||||
return;
|
||||
}
|
||||
if (widget.image != null && widget.image != _base && _incoming == null) {
|
||||
_base = widget.image;
|
||||
_baseErrorKey = widget.imageErrorKey;
|
||||
@@ -336,12 +373,27 @@ class _BackdropArtworkCrossfadeState extends State<_BackdropArtworkCrossfade> wi
|
||||
}
|
||||
|
||||
_currentKey = widget.artworkKey;
|
||||
if (widget.pending) {
|
||||
if (_base == null && _incoming != null && _fade.value == 1) {
|
||||
_base = _incoming;
|
||||
_baseErrorKey = _incomingErrorKey;
|
||||
}
|
||||
setState(() {
|
||||
_fade.stop();
|
||||
_dropIncoming();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (widget.image != null && widget.image == _base) {
|
||||
_baseErrorKey = widget.imageErrorKey;
|
||||
_dropIncoming();
|
||||
return;
|
||||
}
|
||||
|
||||
_transitionToIncoming();
|
||||
}
|
||||
|
||||
void _transitionToIncoming() {
|
||||
setState(() {
|
||||
_fade.stop();
|
||||
_fade.value = 0;
|
||||
|
||||
@@ -52,11 +52,7 @@ class DesktopAppBarSections {
|
||||
final canPop = parentRoute?.canPop ?? false;
|
||||
|
||||
if (canPop) {
|
||||
effectiveLeading = AppBarBackButton(
|
||||
style: BackButtonStyle.plain,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
semanticLabel: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
);
|
||||
effectiveLeading = AppBarBackButton(style: BackButtonStyle.plain, onPressed: () => Navigator.of(context).pop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ class FocusBuilders {
|
||||
required FocusNode focusNode,
|
||||
required KeyEventResult Function(FocusNode, KeyEvent) onKeyEvent,
|
||||
required VoidCallback onTap,
|
||||
required String semanticLabel,
|
||||
bool? selected,
|
||||
required EdgeInsetsGeometry padding,
|
||||
required Color backgroundColor,
|
||||
double borderRadius = 20,
|
||||
@@ -39,6 +41,12 @@ class FocusBuilders {
|
||||
return Focus(
|
||||
focusNode: focusNode,
|
||||
onKeyEvent: onKeyEvent,
|
||||
child: Semantics(
|
||||
label: semanticLabel,
|
||||
button: true,
|
||||
selected: selected,
|
||||
onTap: onTap,
|
||||
excludeSemantics: true,
|
||||
child: ClickableCursor(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
@@ -51,6 +59,7 @@ class FocusBuilders {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ class _FocusableFilterChipState extends State<FocusableFilterChip> with Focusabl
|
||||
focusNode: focusNode,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
onTap: widget.onPressed,
|
||||
semanticLabel: widget.label,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
backgroundColor: backgroundColor,
|
||||
child: Row(
|
||||
|
||||
@@ -16,6 +16,9 @@ class FocusableMediaCard extends StatefulWidget {
|
||||
/// Either a [MediaItem] or a [MediaPlaylist]. Typed as [Object] because
|
||||
/// Dart has no nominal union type. Forwarded as-is to the inner [MediaCard].
|
||||
final Object item;
|
||||
|
||||
/// Optional row/column position announced with this card.
|
||||
final String? semanticValue;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
@@ -78,6 +81,7 @@ class FocusableMediaCard extends StatefulWidget {
|
||||
const FocusableMediaCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.semanticValue,
|
||||
this.width,
|
||||
this.height,
|
||||
this.onRefresh,
|
||||
@@ -134,6 +138,7 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
||||
child: MediaCard(
|
||||
key: _mediaCardKey,
|
||||
item: widget.item,
|
||||
semanticValue: widget.semanticValue,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
onRefresh: widget.onRefresh,
|
||||
|
||||
@@ -174,6 +174,8 @@ class _FocusableTabChipState extends State<FocusableTabChip> with FocusableChipS
|
||||
focusNode: focusNode,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
onTap: widget.onSelect,
|
||||
semanticLabel: widget.label,
|
||||
selected: widget.isSelected,
|
||||
padding: hasImage ? const EdgeInsets.all(8) : const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
backgroundColor: backgroundColor,
|
||||
borderRadius: hasImage ? 12 : 20,
|
||||
|
||||
@@ -49,6 +49,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
|
||||
bool _handleKeyEvent(KeyEvent keyEvent) {
|
||||
if (!widget.enabled) return false;
|
||||
if (keyEvent.logicalKey.isBackKey) return false;
|
||||
if (keyEvent is KeyUpEvent) return false;
|
||||
|
||||
final physicalKeysPressed = HardwareKeyboard.instance.physicalKeysPressed;
|
||||
|
||||
@@ -46,6 +46,7 @@ enum HubCardSizing {
|
||||
/// - Focus never "escapes" to random elements
|
||||
class HubSection extends StatefulWidget {
|
||||
final MediaHub hub;
|
||||
final HubFocusMemory focusMemory;
|
||||
final IconData icon;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
final VoidCallback? onRemoveFromContinueWatching;
|
||||
@@ -94,6 +95,7 @@ class HubSection extends StatefulWidget {
|
||||
const HubSection({
|
||||
super.key,
|
||||
required this.hub,
|
||||
required this.focusMemory,
|
||||
required this.icon,
|
||||
this.onRefresh,
|
||||
this.onRemoveFromContinueWatching,
|
||||
@@ -199,7 +201,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
final clamped = index.clamp(0, _totalItemCount - 1).toInt();
|
||||
_focusedIndex = clamped;
|
||||
// Remember this position for this specific hub
|
||||
HubFocusMemory.setForHub(_focusMemoryKey, clamped);
|
||||
widget.focusMemory.setForHub(_focusMemoryKey, clamped);
|
||||
_notifyFocusedItemChanged();
|
||||
_scrollToIndex(clamped);
|
||||
_hubFocusNode.requestFocus();
|
||||
@@ -211,7 +213,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
|
||||
/// Request focus using the stored memory for this hub
|
||||
void requestFocusFromMemory() {
|
||||
final index = HubFocusMemory.getForHub(_focusMemoryKey, _totalItemCount);
|
||||
final index = widget.focusMemory.getForHub(_focusMemoryKey, _totalItemCount);
|
||||
requestFocusAt(index);
|
||||
}
|
||||
|
||||
@@ -286,7 +288,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
setState(() {
|
||||
_focusedIndex--;
|
||||
});
|
||||
HubFocusMemory.setForHub(_focusMemoryKey, _focusedIndex);
|
||||
widget.focusMemory.setForHub(_focusMemoryKey, _focusedIndex);
|
||||
_notifyFocusedItemChanged();
|
||||
_scrollToIndex(_focusedIndex);
|
||||
} else if (widget.onNavigateToSidebar != null) {
|
||||
@@ -303,7 +305,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
setState(() {
|
||||
_focusedIndex++;
|
||||
});
|
||||
HubFocusMemory.setForHub(_focusMemoryKey, _focusedIndex);
|
||||
widget.focusMemory.setForHub(_focusMemoryKey, _focusedIndex);
|
||||
_notifyFocusedItemChanged();
|
||||
_scrollToIndex(_focusedIndex);
|
||||
}
|
||||
@@ -708,7 +710,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
setState(() {
|
||||
_focusedIndex = clamped;
|
||||
});
|
||||
HubFocusMemory.setForHub(_focusMemoryKey, clamped);
|
||||
widget.focusMemory.setForHub(_focusMemoryKey, clamped);
|
||||
_notifyFocusedItemChanged();
|
||||
_scrollToIndex(clamped);
|
||||
_hubFocusNode.requestFocus();
|
||||
|
||||
+123
-53
@@ -119,6 +119,9 @@ class MediaCard extends StatefulWidget {
|
||||
/// Either a [MediaItem] or a [MediaPlaylist]. Typed as [Object] because Dart
|
||||
/// has no nominal union type — runtime `is` checks select the variant.
|
||||
final Object item;
|
||||
|
||||
/// Optional collection position announced with the card.
|
||||
final String? semanticValue;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
@@ -148,6 +151,7 @@ class MediaCard extends StatefulWidget {
|
||||
const MediaCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.semanticValue,
|
||||
this.width,
|
||||
this.height,
|
||||
this.onRefresh,
|
||||
@@ -298,13 +302,14 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
}
|
||||
|
||||
final semanticLabel = mediaCardSemanticLabel(item);
|
||||
final enableDetailLinks = widget.onTap == null;
|
||||
final preserveDetailSemantics = enableDetailLinks && item is MediaItem && _hasPointerDetailLinks(item);
|
||||
final localPosterPath = _getLocalPosterPath(context, item);
|
||||
|
||||
final cardWidget = viewMode == ViewMode.grid
|
||||
? _buildGridCard(context, item, localPosterPath)
|
||||
Widget cardWidget = viewMode == ViewMode.grid
|
||||
? _buildGridCard(context, item, localPosterPath, preserveDetailSemantics: preserveDetailSemantics)
|
||||
: _MediaCardList(
|
||||
item: item,
|
||||
semanticLabel: semanticLabel,
|
||||
onTap: () => _handleTap(context, item),
|
||||
onTapDown: storeTapPosition,
|
||||
onLongPress: showContextMenuFromTap,
|
||||
@@ -316,7 +321,19 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
showServerName: widget.showServerName,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
cardShapeOverride: widget.cardShapeOverride,
|
||||
enableDetailLinks: widget.onTap == null,
|
||||
enableDetailLinks: enableDetailLinks,
|
||||
);
|
||||
|
||||
cardWidget = Semantics(
|
||||
container: preserveDetailSemantics,
|
||||
explicitChildNodes: preserveDetailSemantics,
|
||||
label: semanticLabel,
|
||||
value: widget.semanticValue,
|
||||
button: true,
|
||||
onTap: handleTap,
|
||||
onLongPress: showContextMenu,
|
||||
excludeSemantics: !preserveDetailSemantics,
|
||||
child: cardWidget,
|
||||
);
|
||||
|
||||
// Catalog stand-ins (Explore tab) have no server-backed actions — every
|
||||
@@ -341,29 +358,32 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
}
|
||||
|
||||
/// Grid layout — inlined from former _MediaCardGrid, _PosterOverlay, and
|
||||
/// flattened Column. Semantics removed (InkWell provides button semantics).
|
||||
/// flattened Column.
|
||||
///
|
||||
/// MergeSemantics collapses the card (texts, progress, button) into ONE
|
||||
/// semantics node. Browse rails/grids show dozens of cards and the
|
||||
/// platform-driven semantics pass runs every frame on TV boxes with an
|
||||
/// accessibility service active — node count is the cost driver. The card
|
||||
/// has a single action (tap; long-press menu), so merging is safe and gives
|
||||
/// screen readers one coherent announcement per card.
|
||||
Widget _buildGridCard(BuildContext context, Object item, String? localPosterPath) {
|
||||
/// Cards without detail links retain one merged semantic node. Cards with
|
||||
/// pointer detail links leave those specific actions as explicit descendants
|
||||
/// of the coherent card announcement.
|
||||
Widget _buildGridCard(
|
||||
BuildContext context,
|
||||
Object item,
|
||||
String? localPosterPath, {
|
||||
required bool preserveDetailSemantics,
|
||||
}) {
|
||||
final Widget card;
|
||||
if (widget.fullBleedImage) {
|
||||
return MergeSemantics(
|
||||
child: LayoutBuilder(
|
||||
card = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final cardWidth = widget.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : null);
|
||||
final cardHeight = widget.height ?? (constraints.hasBoundedHeight ? constraints.maxHeight : null);
|
||||
if (cardHeight == null) return _buildStandardGridCard(context, item, localPosterPath);
|
||||
return _buildFullBleedGridCard(context, item, localPosterPath, width: cardWidth, height: cardHeight);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
card = _buildStandardGridCard(context, item, localPosterPath);
|
||||
}
|
||||
|
||||
return MergeSemantics(child: _buildStandardGridCard(context, item, localPosterPath));
|
||||
return preserveDetailSemantics ? card : MergeSemantics(child: card);
|
||||
}
|
||||
|
||||
Widget _buildFullBleedGridCard(
|
||||
@@ -383,6 +403,7 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
onSecondaryTapDown: storeTapPosition,
|
||||
onSecondaryTap: showContextMenuFromTap,
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: ExcludeSemantics(
|
||||
child: CardFocusBorder(
|
||||
borderRadius: _posterFocusRadius(context, item),
|
||||
child: _clipPosterImage(
|
||||
@@ -408,6 +429,7 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,7 +440,8 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
|
||||
// The focus border hugs the poster (captions stay outside it), matching
|
||||
// the full-bleed card treatment.
|
||||
final poster = CardFocusBorder(
|
||||
final poster = ExcludeSemantics(
|
||||
child: CardFocusBorder(
|
||||
borderRadius: _posterFocusRadius(context, item),
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -440,6 +463,7 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
if (item is MediaItem && _showsWatchedIndicator(item)) WatchedIndicator(item: item),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
@@ -471,12 +495,14 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
onTap: () => _navigateToFocusedDetail(context, item, isOffline: widget.isOffline),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
item is MediaPlaylist ? item.title : (item as MediaItem).displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: const TextStyle(fontWeight: .w600, fontSize: 13, height: 1.1),
|
||||
),
|
||||
),
|
||||
// Subtitle
|
||||
if (item is MediaPlaylist)
|
||||
_MediaCardHelpers.buildPlaylistMeta(context, item)
|
||||
@@ -498,7 +524,6 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
class _MediaCardList extends StatelessWidget {
|
||||
/// Either a [MediaItem] or a [MediaPlaylist].
|
||||
final Object item;
|
||||
final String semanticLabel;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
final void Function(TapDownDetails)? onTapDown;
|
||||
@@ -514,7 +539,6 @@ class _MediaCardList extends StatelessWidget {
|
||||
|
||||
const _MediaCardList({
|
||||
required this.item,
|
||||
required this.semanticLabel,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
this.onTapDown,
|
||||
@@ -668,11 +692,13 @@ class _MediaCardList extends StatelessWidget {
|
||||
onTap: () => _navigateToFocusedDetail(context, mi, isOffline: isOffline),
|
||||
)
|
||||
else
|
||||
Text('S${mi.parentIndex}', style: style),
|
||||
Text('$episodeNum · ', style: style),
|
||||
ExcludeSemantics(child: Text('S${mi.parentIndex}', style: style)),
|
||||
ExcludeSemantics(child: Text('$episodeNum · ', style: style)),
|
||||
Expanded(
|
||||
child: ExcludeSemantics(
|
||||
child: Text(episodeTitle, maxLines: 1, overflow: .ellipsis, style: style),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -699,7 +725,8 @@ class _MediaCardList extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
SizedBox(
|
||||
ExcludeSemantics(
|
||||
child: SizedBox(
|
||||
width: _posterWidth(),
|
||||
height: _posterHeight(),
|
||||
child: Stack(
|
||||
@@ -721,6 +748,7 @@ class _MediaCardList extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -734,15 +762,18 @@ class _MediaCardList extends StatelessWidget {
|
||||
onTap: () => _navigateToFocusedDetail(context, item as MediaItem, isOffline: isOffline),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
_displayTitle(),
|
||||
maxLines: 2,
|
||||
overflow: .ellipsis,
|
||||
style: TextStyle(fontWeight: .w600, fontSize: _titleFontSize, height: 1.2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (metadataLine.isNotEmpty) ...[
|
||||
Text(
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
metadataLine,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
@@ -752,6 +783,7 @@ class _MediaCardList extends StatelessWidget {
|
||||
fontWeight: .w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
if (item is MediaItem &&
|
||||
@@ -761,7 +793,8 @@ class _MediaCardList extends StatelessWidget {
|
||||
_buildEpisodeSubtitle(context, item as MediaItem),
|
||||
const SizedBox(height: 4),
|
||||
] else if (subtitle != null) ...[
|
||||
Text(
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
@@ -770,13 +803,15 @@ class _MediaCardList extends StatelessWidget {
|
||||
fontSize: _subtitleFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
if (!(item is MediaItem &&
|
||||
SettingsService.instance.read(SettingsService.hideSpoilers) &&
|
||||
(item as MediaItem).shouldHideSpoiler) &&
|
||||
_summary() != null) ...[
|
||||
Text(
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
_summary()!,
|
||||
maxLines: _summaryMaxLines,
|
||||
overflow: .ellipsis,
|
||||
@@ -786,10 +821,12 @@ class _MediaCardList extends StatelessWidget {
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showServerName && item is MediaItem && (item as MediaItem).serverName != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
ExcludeSemantics(
|
||||
child: Row(
|
||||
children: [
|
||||
BackendBadge(
|
||||
backend: (item as MediaItem).backend,
|
||||
@@ -810,6 +847,7 @@ class _MediaCardList extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -1001,13 +1039,15 @@ Widget _buildPosterImage(
|
||||
class _MediaCardHelpers {
|
||||
static Widget buildPlaylistMeta(BuildContext context, MediaPlaylist playlist) {
|
||||
if (playlist.leafCount != null && playlist.leafCount! > 0) {
|
||||
return Text(
|
||||
return ExcludeSemantics(
|
||||
child: Text(
|
||||
t.playlists.itemCount(count: playlist.leafCount!),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: tokens(context).textMuted, fontSize: 11, height: 1.1),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
@@ -1028,25 +1068,31 @@ class _MediaCardHelpers {
|
||||
if (mi.kind == MediaKind.collection) {
|
||||
final count = mi.childCount ?? mi.leafCount;
|
||||
if (count != null && count > 0) {
|
||||
return Text(
|
||||
return ExcludeSemantics(
|
||||
child: Text(
|
||||
t.playlists.itemCount(count: count),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For albums, show the album artist
|
||||
if (mi.kind == MediaKind.album && mi.albumArtistTitle != null) {
|
||||
return Text(mi.albumArtistTitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle);
|
||||
return ExcludeSemantics(
|
||||
child: Text(mi.albumArtistTitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
);
|
||||
}
|
||||
|
||||
// For tracks, show "Artist • duration"
|
||||
if (mi.kind == MediaKind.track) {
|
||||
final parts = [?mi.trackArtistTitle, if (mi.durationMs case final durationMs?) formatDurationTextual(durationMs)];
|
||||
if (parts.isNotEmpty) {
|
||||
return Text(parts.join(' • '), maxLines: 1, overflow: .ellipsis, style: subtitleStyle);
|
||||
return ExcludeSemantics(
|
||||
child: Text(parts.join(' • '), maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1063,33 +1109,43 @@ class _MediaCardHelpers {
|
||||
style: subtitleStyle,
|
||||
onTap: () => _navigateToFocusedDetail(context, mi, isOffline: isOffline),
|
||||
),
|
||||
Text('$episodeSuffix · ', style: subtitleStyle),
|
||||
ExcludeSemantics(child: Text('$episodeSuffix · ', style: subtitleStyle)),
|
||||
Expanded(
|
||||
child: ExcludeSemantics(
|
||||
child: Text(episodeTitle, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
return ExcludeSemantics(
|
||||
child: Text(
|
||||
'S${mi.parentIndex}$episodeSuffix · $episodeTitle',
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For other media types, show subtitle/parent/year
|
||||
if (mi.displaySubtitle != null) {
|
||||
return Text(mi.displaySubtitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle);
|
||||
return ExcludeSemantics(
|
||||
child: Text(mi.displaySubtitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
);
|
||||
} else if (mi.parentTitle != null) {
|
||||
return Text(mi.parentTitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle);
|
||||
return ExcludeSemantics(
|
||||
child: Text(mi.parentTitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
);
|
||||
} else if (mi.year != null) {
|
||||
final edition = mi.editionTitle;
|
||||
return Text(
|
||||
return ExcludeSemantics(
|
||||
child: Text(
|
||||
edition != null ? '${mi.year} · $edition' : '${mi.year}',
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1097,6 +1153,12 @@ class _MediaCardHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the card renders any pointer detail link for this item.
|
||||
bool _hasPointerDetailLinks(MediaItem mi) {
|
||||
if (_hasClickableTitle(mi)) return true;
|
||||
return mi.isEpisode && mi.parentIndex != null && mi.parentId != null;
|
||||
}
|
||||
|
||||
/// Whether this media item has a clickable title that navigates somewhere.
|
||||
/// Episodes/seasons navigate to their parent show; movies navigate to their detail page.
|
||||
bool _hasClickableTitle(MediaItem mi) {
|
||||
@@ -1111,7 +1173,8 @@ void _navigateToFocusedDetail(BuildContext context, MediaItem item, {bool isOffl
|
||||
}
|
||||
|
||||
/// Text widget that shows hover underline + pointer cursor only in pointer mode.
|
||||
/// In keyboard/dpad mode, renders as plain text with no interaction.
|
||||
/// Keyboard/dpad mode keeps plain visual text while screen readers retain the
|
||||
/// separately invokable detail action.
|
||||
class _ClickableText extends StatefulWidget {
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
@@ -1130,27 +1193,32 @@ class _ClickableTextState extends State<_ClickableText> {
|
||||
Widget build(BuildContext context) {
|
||||
final isKeyboard = InputModeTracker.isKeyboardMode(context);
|
||||
final baseStyle = widget.style ?? const TextStyle();
|
||||
final text = Semantics(
|
||||
label: widget.text,
|
||||
hint: t.mediaMenu.viewDetails,
|
||||
button: true,
|
||||
onTap: widget.onTap,
|
||||
excludeSemantics: true,
|
||||
child: Text(
|
||||
widget.text,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: isKeyboard
|
||||
? baseStyle
|
||||
: baseStyle.copyWith(
|
||||
decoration: _isHovered ? TextDecoration.underline : null,
|
||||
decorationColor: baseStyle.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (isKeyboard) {
|
||||
return Text(widget.text, maxLines: 1, overflow: .ellipsis, style: baseStyle);
|
||||
}
|
||||
if (isKeyboard) return text;
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Text(
|
||||
widget.text,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: baseStyle.copyWith(
|
||||
decoration: _isHovered ? TextDecoration.underline : null,
|
||||
decorationColor: baseStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: GestureDetector(excludeFromSemantics: true, onTap: widget.onTap, child: text),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1204,6 +1272,7 @@ class _CardTapRegion extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
if (!PlatformDetector.isDesktopOS()) {
|
||||
return GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onTapDown: onTapDown,
|
||||
@@ -1214,6 +1283,7 @@ class _CardTapRegion extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
excludeFromSemantics: true,
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
canRequestFocus: false,
|
||||
onTap: onTap,
|
||||
|
||||
@@ -43,6 +43,7 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
final IconData? fallbackIcon;
|
||||
final ImageType imageType;
|
||||
final String? localFilePath;
|
||||
final bool cacheMissingLocalFile;
|
||||
|
||||
const OptimizedMediaImage._({
|
||||
super.key,
|
||||
@@ -61,6 +62,7 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
this.fallbackIcon,
|
||||
this.imageType = ImageType.poster,
|
||||
this.localFilePath,
|
||||
this.cacheMissingLocalFile = false,
|
||||
});
|
||||
|
||||
/// Generic constructor for optimized images.
|
||||
@@ -81,6 +83,7 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
IconData? fallbackIcon,
|
||||
ImageType imageType,
|
||||
String? localFilePath,
|
||||
bool cacheMissingLocalFile,
|
||||
}) = OptimizedMediaImage._;
|
||||
|
||||
/// Named constructor for poster images with default fallback icon.
|
||||
@@ -199,16 +202,27 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final path = localFilePath;
|
||||
if (path == null) {
|
||||
return _buildResolved(context, _LocalFileResolution.missing, null);
|
||||
return _buildResolved(context, LocalFileResolution.missing, null);
|
||||
}
|
||||
return _ResolvedLocalFile(path: path, builder: _buildResolved);
|
||||
return ResolvedLocalFile(path: path, cacheMissing: cacheMissingLocalFile, builder: _buildResolved);
|
||||
}
|
||||
|
||||
Widget _buildResolved(BuildContext context, _LocalFileResolution resolution, File? localFile) {
|
||||
if (resolution == _LocalFileResolution.pending) return _surfacePlaceholder(context);
|
||||
final hasLocal = resolution == _LocalFileResolution.present;
|
||||
Widget _buildResolved(BuildContext context, LocalFileResolution resolution, File? localFile) {
|
||||
if (resolution == LocalFileResolution.pending) {
|
||||
return placeholder == null
|
||||
? _surfacePlaceholder(context)
|
||||
: _buildPlaceholder(context, imagePath ?? localFilePath ?? '');
|
||||
}
|
||||
final hasLocal = resolution == LocalFileResolution.present;
|
||||
|
||||
if (!hasLocal && (imagePath == null || imagePath!.isEmpty)) {
|
||||
if (errorWidget != null) {
|
||||
return errorWidget!(
|
||||
context,
|
||||
localFilePath ?? '',
|
||||
FileSystemException('Local image file is unavailable', localFilePath),
|
||||
);
|
||||
}
|
||||
return _buildFallback(context);
|
||||
}
|
||||
|
||||
@@ -484,22 +498,43 @@ class _FadeInNetworkImageState extends State<_FadeInNetworkImage> with SingleTic
|
||||
}
|
||||
}
|
||||
|
||||
enum _LocalFileResolution { pending, missing, present }
|
||||
enum LocalFileResolution { pending, missing, present }
|
||||
|
||||
class _ResolvedLocalFile extends StatefulWidget {
|
||||
const _ResolvedLocalFile({required this.path, required this.builder});
|
||||
typedef LocalFileResolutionBuilder = Widget Function(BuildContext context, LocalFileResolution resolution, File? file);
|
||||
|
||||
Future<bool> _defaultLocalFileExists(File file) => file.exists();
|
||||
|
||||
/// Resolves local file availability without blocking the build isolate.
|
||||
///
|
||||
/// Results are scoped to this widget state and keyed by [path]. Present files
|
||||
/// are always cached. Missing files are cached only when [cacheMissing] is set,
|
||||
/// allowing consumers that expect late file creation to retry on rebuild.
|
||||
class ResolvedLocalFile extends StatefulWidget {
|
||||
const ResolvedLocalFile({
|
||||
super.key,
|
||||
required this.path,
|
||||
required this.builder,
|
||||
this.cacheMissing = false,
|
||||
this.fileExists = _defaultLocalFileExists,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final Widget Function(BuildContext context, _LocalFileResolution resolution, File? file) builder;
|
||||
final LocalFileResolutionBuilder builder;
|
||||
final bool cacheMissing;
|
||||
|
||||
@visibleForTesting
|
||||
final Future<bool> Function(File file) fileExists;
|
||||
|
||||
@override
|
||||
State<_ResolvedLocalFile> createState() => _ResolvedLocalFileState();
|
||||
State<ResolvedLocalFile> createState() => _ResolvedLocalFileState();
|
||||
}
|
||||
|
||||
class _ResolvedLocalFileState extends State<_ResolvedLocalFile> {
|
||||
class _ResolvedLocalFileState extends State<ResolvedLocalFile> {
|
||||
final Map<String, File> _presentFiles = <String, File>{};
|
||||
final Set<String> _missingFiles = <String>{};
|
||||
final Set<String> _pendingPaths = <String>{};
|
||||
File? _file;
|
||||
_LocalFileResolution _resolution = _LocalFileResolution.pending;
|
||||
int _generation = 0;
|
||||
LocalFileResolution _resolution = LocalFileResolution.pending;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -508,28 +543,74 @@ class _ResolvedLocalFileState extends State<_ResolvedLocalFile> {
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_ResolvedLocalFile oldWidget) {
|
||||
void didUpdateWidget(ResolvedLocalFile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.path != widget.path || _resolution == _LocalFileResolution.missing) _resolve();
|
||||
if (oldWidget.fileExists != widget.fileExists) {
|
||||
_presentFiles.clear();
|
||||
_missingFiles.clear();
|
||||
_pendingPaths.clear();
|
||||
} else if (oldWidget.cacheMissing && !widget.cacheMissing) {
|
||||
_missingFiles.clear();
|
||||
}
|
||||
if (oldWidget.path != widget.path ||
|
||||
oldWidget.fileExists != widget.fileExists ||
|
||||
_resolution == LocalFileResolution.missing) {
|
||||
_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
void _resolve() {
|
||||
final generation = ++_generation;
|
||||
final path = widget.path;
|
||||
final present = _presentFiles[path];
|
||||
if (present != null) {
|
||||
_file = present;
|
||||
_resolution = LocalFileResolution.present;
|
||||
return;
|
||||
}
|
||||
if (widget.cacheMissing && _missingFiles.contains(path)) {
|
||||
_file = null;
|
||||
_resolution = _LocalFileResolution.pending;
|
||||
final candidate = File(widget.path);
|
||||
candidate.exists().then((exists) {
|
||||
if (!mounted || generation != _generation) return;
|
||||
_resolution = LocalFileResolution.missing;
|
||||
return;
|
||||
}
|
||||
|
||||
_file = null;
|
||||
_resolution = LocalFileResolution.pending;
|
||||
if (!_pendingPaths.add(path)) return;
|
||||
|
||||
final candidate = File(path);
|
||||
try {
|
||||
widget
|
||||
.fileExists(candidate)
|
||||
.then(
|
||||
(exists) => _complete(path, candidate, exists),
|
||||
onError: (Object _, StackTrace _) => _complete(path, candidate, false),
|
||||
);
|
||||
} catch (_) {
|
||||
_complete(path, candidate, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _complete(String path, File candidate, bool exists) {
|
||||
if (!mounted) return;
|
||||
_pendingPaths.remove(path);
|
||||
if (exists) {
|
||||
_presentFiles[path] = candidate;
|
||||
_missingFiles.remove(path);
|
||||
} else if (widget.cacheMissing) {
|
||||
_missingFiles.add(path);
|
||||
}
|
||||
if (widget.path != path) return;
|
||||
setState(() {
|
||||
_file = exists ? candidate : null;
|
||||
_resolution = exists ? _LocalFileResolution.present : _LocalFileResolution.missing;
|
||||
});
|
||||
_resolution = exists ? LocalFileResolution.present : LocalFileResolution.missing;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
++_generation;
|
||||
_presentFiles.clear();
|
||||
_missingFiles.clear();
|
||||
_pendingPaths.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../exceptions/media_server_exceptions.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import '../models/plex/plex_activity.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../utils/media_server_http_client.dart';
|
||||
|
||||
class ServerActivitiesButton extends StatefulWidget {
|
||||
const ServerActivitiesButton({super.key});
|
||||
@@ -43,6 +45,8 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
OverlayEntry? _overlayEntry;
|
||||
final _panelNotifier = ValueNotifier<_PanelData>(_PanelData.loading);
|
||||
Timer? _pollTimer;
|
||||
AbortController? _activeLoadAbort;
|
||||
int _loadGeneration = 0;
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
@@ -60,6 +64,10 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
void _removeOverlay() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
_loadGeneration++;
|
||||
final activeLoadAbort = _activeLoadAbort;
|
||||
_activeLoadAbort = null;
|
||||
activeLoadAbort?.abort();
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
@@ -84,10 +92,10 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
builder: (_) => _buildOverlay(right: right, top: top),
|
||||
);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
_fetchActivities();
|
||||
_startRefresh(silent: false);
|
||||
}
|
||||
|
||||
Future<List<_ServerResult>> _loadFromServers() async {
|
||||
Future<List<_ServerResult>> _loadFromServers(AbortController abort) async {
|
||||
final multiServer = Provider.of<MultiServerProvider>(context, listen: false);
|
||||
final serverIds = multiServer.onlineServerIds;
|
||||
|
||||
@@ -95,7 +103,7 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
// Plex-only: `/activities` API is Plex-specific.
|
||||
final client = multiServer.getPlexClientForServer(ServerId(serverId));
|
||||
if (client == null) return null;
|
||||
final activities = await client.getActivities();
|
||||
final activities = await client.getActivities(abort: abort);
|
||||
return _ServerResult(serverId: serverId, serverName: client.serverName ?? serverId, activities: activities);
|
||||
});
|
||||
|
||||
@@ -103,32 +111,70 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
return rawResults.whereType<_ServerResult>().toList();
|
||||
}
|
||||
|
||||
Future<void> _fetchActivities() async {
|
||||
void _startRefresh({required bool silent}) {
|
||||
if (!mounted || _overlayEntry == null) return;
|
||||
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
final generation = ++_loadGeneration;
|
||||
final supersededAbort = _activeLoadAbort;
|
||||
final abort = AbortController();
|
||||
_activeLoadAbort = abort;
|
||||
supersededAbort?.abort();
|
||||
|
||||
if (!silent) {
|
||||
_panelNotifier.value = _PanelData.loading;
|
||||
}
|
||||
unawaited(_runRefresh(generation: generation, abort: abort, silent: silent));
|
||||
}
|
||||
|
||||
Future<void> _runRefresh({required int generation, required AbortController abort, required bool silent}) async {
|
||||
var scheduleNext = false;
|
||||
try {
|
||||
final results = await _loadFromServers();
|
||||
if (!mounted) return;
|
||||
final results = await _loadFromServers(abort);
|
||||
if (!_ownsLoad(generation, abort)) return;
|
||||
_panelNotifier.value = _PanelData(fetchState: _FetchState.loaded, results: results);
|
||||
_startPolling();
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
scheduleNext = true;
|
||||
} on MediaServerHttpException catch (error) {
|
||||
if (error.isCancellation || !_ownsLoad(generation, abort)) return;
|
||||
if (silent) {
|
||||
scheduleNext = true;
|
||||
} else {
|
||||
_panelNotifier.value = const _PanelData(fetchState: _FetchState.error, results: []);
|
||||
}
|
||||
} catch (_) {
|
||||
if (!_ownsLoad(generation, abort)) return;
|
||||
if (silent) {
|
||||
scheduleNext = true;
|
||||
} else {
|
||||
_panelNotifier.value = const _PanelData(fetchState: _FetchState.error, results: []);
|
||||
}
|
||||
} finally {
|
||||
if (identical(_activeLoadAbort, abort)) {
|
||||
_activeLoadAbort = null;
|
||||
if (scheduleNext && _isCurrentGeneration(generation)) {
|
||||
_schedulePoll(generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
bool _ownsLoad(int generation, AbortController abort) {
|
||||
return _isCurrentGeneration(generation) && identical(_activeLoadAbort, abort);
|
||||
}
|
||||
|
||||
bool _isCurrentGeneration(int generation) {
|
||||
return mounted && _overlayEntry != null && _loadGeneration == generation;
|
||||
}
|
||||
|
||||
void _schedulePoll(int generation) {
|
||||
_pollTimer?.cancel();
|
||||
if (mounted && _overlayEntry != null) {
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 3), (_) => _silentRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _silentRefresh() async {
|
||||
if (!mounted) return;
|
||||
try {
|
||||
final results = await _loadFromServers();
|
||||
if (!mounted) return;
|
||||
_panelNotifier.value = _PanelData(fetchState: _FetchState.loaded, results: results);
|
||||
} catch (_) {}
|
||||
if (!_isCurrentGeneration(generation)) return;
|
||||
_pollTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (!_isCurrentGeneration(generation)) return;
|
||||
_pollTimer = null;
|
||||
_startRefresh(silent: true);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _cancelActivity(ServerId serverId, String uuid) async {
|
||||
@@ -142,10 +188,7 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
|
||||
return;
|
||||
}
|
||||
if (!mounted || _overlayEntry == null) return;
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
_panelNotifier.value = _PanelData.loading;
|
||||
unawaited(_fetchActivities());
|
||||
_startRefresh(silent: false);
|
||||
}
|
||||
|
||||
Widget _buildOverlay({required double right, required double top}) {
|
||||
|
||||
@@ -49,8 +49,8 @@ class SettingSwitchTile extends StatelessWidget {
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
value: value,
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
onChanged: enabled
|
||||
? (v) async {
|
||||
await svc.write(pref, v);
|
||||
@@ -93,8 +93,8 @@ class SettingNavigationTile extends StatelessWidget {
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
trailing: AppIcon(trailingIcon, fill: 1),
|
||||
onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,8 @@ class SettingNumberTile extends StatelessWidget {
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitleBuilder(value)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
onTap: () => showNumericInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
@@ -192,8 +192,8 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitleBuilder(value)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
onTap: () async {
|
||||
final picked = await showSelectionDialog<T>(
|
||||
context: context,
|
||||
@@ -241,8 +241,8 @@ class SettingRegexTile extends StatelessWidget {
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
onTap: () => showRegexInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
@@ -341,8 +341,8 @@ class SettingColorTile extends StatelessWidget {
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
dense: settingsRowDense(),
|
||||
visualDensity: settingsRowVisualDensity(),
|
||||
dense: settingsRowDense(context),
|
||||
visualDensity: settingsRowVisualDensity(context),
|
||||
onTap: () => showColorInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
|
||||
@@ -6,21 +6,19 @@ import 'expressive_button_group.dart';
|
||||
|
||||
/// Standard settings-option title style.
|
||||
///
|
||||
/// Matches Flutter's dense ListTile title size so custom controls and
|
||||
/// ordinary settings rows keep the same hierarchy. TV retains the larger
|
||||
/// body style used for D-pad readability at distance.
|
||||
/// Mobile uses Flutter's compact ListTile title size. Desktop and TV retain
|
||||
/// the larger body style used for pointer and D-pad readability.
|
||||
TextStyle? settingsOptionTitleStyle(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.bodyLarge;
|
||||
return PlatformDetector.isTV() ? style : style?.copyWith(fontSize: 13);
|
||||
return PlatformDetector.isMobile(context) ? style?.copyWith(fontSize: 13) : style;
|
||||
}
|
||||
|
||||
/// Standard settings-row density, paired with [settingsOptionTitleStyle]:
|
||||
/// compact rows everywhere except TV, where full-height rows keep D-pad
|
||||
/// readability.
|
||||
bool settingsRowDense() => !PlatformDetector.isTV();
|
||||
/// compact on mobile and full-height on desktop and TV.
|
||||
bool settingsRowDense(BuildContext context) => PlatformDetector.isMobile(context);
|
||||
|
||||
VisualDensity settingsRowVisualDensity() =>
|
||||
settingsRowDense() ? const VisualDensity(vertical: -3) : VisualDensity.standard;
|
||||
VisualDensity settingsRowVisualDensity(BuildContext context) =>
|
||||
settingsRowDense(context) ? const VisualDensity(vertical: -3) : VisualDensity.standard;
|
||||
|
||||
class SettingsSectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
@@ -306,6 +306,7 @@ enum TvRailTrailing { none, loading, error, viewAll }
|
||||
|
||||
class TvBrowseRail extends StatefulWidget {
|
||||
final List<MediaHub> hubs;
|
||||
final HubFocusMemory focusMemory;
|
||||
final IconData Function(MediaHub hub, int index) iconForHub;
|
||||
|
||||
/// Whether to show each hub's originating server name in its header. Used when
|
||||
@@ -354,6 +355,7 @@ class TvBrowseRail extends StatefulWidget {
|
||||
const TvBrowseRail({
|
||||
super.key,
|
||||
required this.hubs,
|
||||
required this.focusMemory,
|
||||
required this.iconForHub,
|
||||
this.showServerName = false,
|
||||
this.onFocusedItemChanged,
|
||||
@@ -774,7 +776,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
final currentHub = _activeHub;
|
||||
if (currentHub != null) _rememberFocus(currentHub);
|
||||
final nextHub = widget.hubs[next];
|
||||
final remembered = HubFocusMemory.getForHubOnly(_hubKey(nextHub), _totalItemCount(nextHub));
|
||||
final remembered = widget.focusMemory.getForHubOnly(_hubKey(nextHub), _totalItemCount(nextHub));
|
||||
// No setState: the active-hub change is observed through _focusModel
|
||||
// selectors (cards, headers, row dim), so a hub move repaints only the
|
||||
// two affected rows instead of rebuilding every visible card. Section
|
||||
@@ -857,7 +859,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
}
|
||||
|
||||
void _rememberFocus(MediaHub hub) {
|
||||
HubFocusMemory.setForHub(_hubKey(hub), _itemIndex);
|
||||
widget.focusMemory.setForHub(_hubKey(hub), _itemIndex);
|
||||
}
|
||||
|
||||
void _scrollToItem({bool animate = true, Duration duration = _navigationScrollDuration}) {
|
||||
@@ -1354,7 +1356,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
}) {
|
||||
final isActiveHub = hubIndex == _hubIndex;
|
||||
final totalCount = _totalItemCount(hub);
|
||||
final inactiveIndex = HubFocusMemory.getForHubOnly(_hubKey(hub), totalCount);
|
||||
final inactiveIndex = widget.focusMemory.getForHubOnly(_hubKey(hub), totalCount);
|
||||
final focusedIndex = isActiveHub ? _itemIndex : inactiveIndex;
|
||||
final scrollController = _scrollControllerForHub(hub, metrics, railViewportWidth, scale, focusedIndex);
|
||||
_metricsByHub[_hubKey(hub)] = metrics;
|
||||
|
||||
@@ -398,7 +398,7 @@ class _TvVirtualKeyboardDialogState extends State<_TvVirtualKeyboardDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
bool _handlePhysicalKeyboardTextInput(KeyEvent event) {
|
||||
@@ -564,22 +564,15 @@ class _TvVirtualKeyboardDialogState extends State<_TvVirtualKeyboardDialog> {
|
||||
void _backspace() {
|
||||
final value = widget.controller.value;
|
||||
final (:start, :end) = _selectionRangeForEdit(value);
|
||||
if (start == end && start == 0) return;
|
||||
|
||||
if (start != end) {
|
||||
final codeUnitRange = start == end ? TextRange(start: start - 1, end: start) : TextRange(start: start, end: end);
|
||||
final range = expandToGraphemeRange(value.text, codeUnitRange);
|
||||
if (range.isCollapsed) return;
|
||||
_replace(
|
||||
value.copyWith(
|
||||
text: value.text.replaceRange(start, end, ''),
|
||||
selection: TextSelection.collapsed(offset: start),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (start == 0) return;
|
||||
|
||||
_replace(
|
||||
value.copyWith(
|
||||
text: value.text.replaceRange(start - 1, start, ''),
|
||||
selection: TextSelection.collapsed(offset: start - 1),
|
||||
text: value.text.replaceRange(range.start, range.end, ''),
|
||||
selection: TextSelection.collapsed(offset: range.start),
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
);
|
||||
@@ -588,23 +581,15 @@ class _TvVirtualKeyboardDialogState extends State<_TvVirtualKeyboardDialog> {
|
||||
void _deleteForward() {
|
||||
final value = widget.controller.value;
|
||||
final (:start, :end) = _selectionRangeForEdit(value);
|
||||
if (start == end && start >= value.text.length) return;
|
||||
|
||||
if (start != end) {
|
||||
final codeUnitRange = start == end ? TextRange(start: start, end: start + 1) : TextRange(start: start, end: end);
|
||||
final range = expandToGraphemeRange(value.text, codeUnitRange);
|
||||
if (range.isCollapsed) return;
|
||||
_replace(
|
||||
value.copyWith(
|
||||
text: value.text.replaceRange(start, end, ''),
|
||||
selection: TextSelection.collapsed(offset: start),
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (start >= value.text.length) return;
|
||||
|
||||
_replace(
|
||||
value.copyWith(
|
||||
text: value.text.replaceRange(start, start + 1, ''),
|
||||
selection: TextSelection.collapsed(offset: start),
|
||||
text: value.text.replaceRange(range.start, range.end, ''),
|
||||
selection: TextSelection.collapsed(offset: range.start),
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,8 +47,8 @@ class TrackControlsState {
|
||||
final VoidCallback? onToggleAlwaysOnTop;
|
||||
final Function(int)? onSwitchVersion;
|
||||
final ValueChanged<TranscodeQualityPreset>? onSwitchQualityPreset;
|
||||
final ValueChanged<int>? onSwitchAudioStreamId;
|
||||
final ValueChanged<PlaybackSourceSubtitleChoice>? onSwitchSubtitle;
|
||||
final Future<void> Function(int)? onSwitchAudioStreamId;
|
||||
final Future<void> Function(PlaybackSourceSubtitleChoice)? onSwitchSubtitle;
|
||||
final Function(AudioTrack)? onAudioTrackChanged;
|
||||
final Function(SubtitleTrack)? onSubtitleTrackChanged;
|
||||
final Function(SubtitleTrack)? onSecondarySubtitleTrackChanged;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
part of '../video_controls.dart';
|
||||
|
||||
final Expando<LatestAsyncWrite<String>> _subtitleVisibilityWrites = Expando<LatestAsyncWrite<String>>();
|
||||
|
||||
extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
|
||||
void _toggleSubtitles() {
|
||||
final currentTrack = widget.player.state.track.subtitle;
|
||||
@@ -19,6 +21,8 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
|
||||
|
||||
void _setSubtitleVisibility(bool visible) {
|
||||
final targetPlayer = widget.player;
|
||||
final coordinator = _subtitleVisibilityWrites[targetPlayer] ??= LatestAsyncWrite<String>();
|
||||
final writeToken = coordinator.begin('sub-visibility');
|
||||
final generation = ++_subtitleVisibilityWriteGeneration;
|
||||
_setControlsState(() {
|
||||
_subtitlesVisible = visible;
|
||||
@@ -26,12 +30,25 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
|
||||
|
||||
unawaited(() async {
|
||||
try {
|
||||
final committed = await coordinator.commitIfLatest('sub-visibility', writeToken, () async {
|
||||
await targetPlayer.setProperty('sub-visibility', visible ? 'yes' : 'no');
|
||||
if (!mounted || generation != _subtitleVisibilityWriteGeneration || targetPlayer != widget.player) return;
|
||||
if (mounted && targetPlayer == widget.player) {
|
||||
// Preserve every successfully executed mutation as the rollback
|
||||
// baseline, even when a newer optimistic toggle is queued.
|
||||
_confirmedSubtitlesVisible = visible;
|
||||
}
|
||||
});
|
||||
if (!committed ||
|
||||
!mounted ||
|
||||
generation != _subtitleVisibilityWriteGeneration ||
|
||||
targetPlayer != widget.player) {
|
||||
return;
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Failed to update subtitle visibility', error: error, stackTrace: stackTrace);
|
||||
if (!mounted || generation != _subtitleVisibilityWriteGeneration || targetPlayer != widget.player) return;
|
||||
if (!mounted || generation != _subtitleVisibilityWriteGeneration || targetPlayer != widget.player) {
|
||||
return;
|
||||
}
|
||||
_setControlsState(() {
|
||||
_subtitlesVisible = _confirmedSubtitlesVisible;
|
||||
});
|
||||
|
||||
@@ -47,7 +47,12 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
|
||||
}
|
||||
|
||||
/// Controls hide delay: 5s on mobile/TV/keyboard-nav, 3s on desktop with mouse.
|
||||
/// Maestro builds extend the delay because accessibility-tree queries can take
|
||||
/// longer than the production timeout on physical devices.
|
||||
Duration get _hideDelay {
|
||||
if (const bool.fromEnvironment('PLEZY_MAESTRO_E2E')) {
|
||||
return const Duration(seconds: 30);
|
||||
}
|
||||
final isMobile = (Platform.isIOS || Platform.isAndroid) && !PlatformDetector.isTV();
|
||||
if (isMobile || PlatformDetector.isTV() || _videoPlayerNavigationEnabled) {
|
||||
return const Duration(seconds: 5);
|
||||
@@ -82,6 +87,15 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
|
||||
widget.chromeController.toggle();
|
||||
}
|
||||
|
||||
void _toggleControlsFromSemantics() {
|
||||
if (_showControls) {
|
||||
widget.chromeController.hide();
|
||||
return;
|
||||
}
|
||||
widget.chromeController.show(restartAutoHide: false);
|
||||
widget.chromeController.cancelAutoHide();
|
||||
}
|
||||
|
||||
/// Apply preferred orientations for the given lock state. Wired to
|
||||
/// [SettingsService.rotationLocked] via [bindEffect] so any change — from
|
||||
/// this toggle or from the settings screen — fires the same SystemChrome call.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
@@ -135,7 +137,7 @@ class TrackSheet extends StatelessWidget {
|
||||
class _SourceAudioColumn extends StatefulWidget {
|
||||
final List<MediaAudioTrack> tracks;
|
||||
final int? selectedStreamId;
|
||||
final ValueChanged<int> onSelected;
|
||||
final Future<void> Function(int) onSelected;
|
||||
final bool showHeader;
|
||||
|
||||
const _SourceAudioColumn({
|
||||
@@ -151,6 +153,7 @@ class _SourceAudioColumn extends StatefulWidget {
|
||||
|
||||
class _SourceAudioColumnState extends State<_SourceAudioColumn> {
|
||||
final _initialScroll = InitialItemScrollController();
|
||||
bool _selectionPending = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -158,6 +161,17 @@ class _SourceAudioColumnState extends State<_SourceAudioColumn> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _select(int streamId) async {
|
||||
if (_selectionPending) return;
|
||||
setState(() => _selectionPending = true);
|
||||
try {
|
||||
await widget.onSelected(streamId);
|
||||
if (mounted) OverlaySheetController.of(context).close();
|
||||
} finally {
|
||||
if (mounted) setState(() => _selectionPending = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedId = _effectiveSelectedStreamId();
|
||||
@@ -167,6 +181,7 @@ class _SourceAudioColumnState extends State<_SourceAudioColumn> {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.showHeader) SheetColumnHeader(label: t.videoControls.audioLabel),
|
||||
if (_selectionPending) const LinearProgressIndicator(minHeight: 2),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _initialScroll.controller,
|
||||
@@ -179,10 +194,7 @@ class _SourceAudioColumnState extends State<_SourceAudioColumn> {
|
||||
key: index == 0 ? _initialScroll.firstItemKey : null,
|
||||
label: track.label,
|
||||
isSelected: isSelected,
|
||||
onTap: () {
|
||||
OverlaySheetController.of(context).close();
|
||||
widget.onSelected(track.id);
|
||||
},
|
||||
onTap: () => unawaited(_select(track.id)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -214,6 +226,7 @@ class _SourceSubtitleColumn extends StatefulWidget {
|
||||
|
||||
class _SourceSubtitleColumnState extends State<_SourceSubtitleColumn> {
|
||||
final _initialScroll = InitialItemScrollController();
|
||||
bool _selectionPending = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -221,6 +234,17 @@ class _SourceSubtitleColumnState extends State<_SourceSubtitleColumn> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _select(PlaybackSourceSubtitleChoice choice) async {
|
||||
if (_selectionPending) return;
|
||||
setState(() => _selectionPending = true);
|
||||
try {
|
||||
await widget.trackControlsState.onSwitchSubtitle!(choice);
|
||||
if (mounted) OverlaySheetController.of(context).close();
|
||||
} finally {
|
||||
if (mounted) setState(() => _selectionPending = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedChoice = _effectiveSelectedChoice();
|
||||
@@ -231,6 +255,7 @@ class _SourceSubtitleColumnState extends State<_SourceSubtitleColumn> {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.showHeader) SheetColumnHeader(label: t.videoControls.subtitlesLabel),
|
||||
if (_selectionPending) const LinearProgressIndicator(minHeight: 2),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _initialScroll.controller,
|
||||
@@ -241,10 +266,7 @@ class _SourceSubtitleColumnState extends State<_SourceSubtitleColumn> {
|
||||
context: context,
|
||||
key: _initialScroll.firstItemKey,
|
||||
isSelected: selectedChoice.isOff,
|
||||
onTap: () {
|
||||
OverlaySheetController.of(context).close();
|
||||
widget.trackControlsState.onSwitchSubtitle!(const PlaybackSourceSubtitleChoice.off());
|
||||
},
|
||||
onTap: () => unawaited(_select(const PlaybackSourceSubtitleChoice.off())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,10 +275,7 @@ class _SourceSubtitleColumnState extends State<_SourceSubtitleColumn> {
|
||||
context: context,
|
||||
label: track.labelForIndex(index - 1),
|
||||
isSelected: track.id == selectedId,
|
||||
onTap: () {
|
||||
OverlaySheetController.of(context).close();
|
||||
widget.trackControlsState.onSwitchSubtitle!(PlaybackSourceSubtitleChoice.source(track.id));
|
||||
},
|
||||
onTap: () => unawaited(_select(PlaybackSourceSubtitleChoice.source(track.id))),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -372,6 +391,7 @@ class _SubtitleColumn extends StatefulWidget {
|
||||
|
||||
class _SubtitleColumnState extends State<_SubtitleColumn> {
|
||||
final _initialScroll = InitialItemScrollController();
|
||||
bool _selectionPending = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -379,6 +399,17 @@ class _SubtitleColumnState extends State<_SubtitleColumn> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectSourceSidecar(int streamId) async {
|
||||
if (_selectionPending) return;
|
||||
setState(() => _selectionPending = true);
|
||||
try {
|
||||
await widget.trackControlsState.onSwitchSubtitle!(PlaybackSourceSubtitleChoice.source(streamId));
|
||||
if (mounted) OverlaySheetController.of(context).close();
|
||||
} finally {
|
||||
if (mounted) setState(() => _selectionPending = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedSub = widget.selection.subtitle;
|
||||
@@ -401,6 +432,7 @@ class _SubtitleColumnState extends State<_SubtitleColumn> {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.showHeader) SheetColumnHeader(label: t.videoControls.subtitlesLabel),
|
||||
if (_selectionPending) const LinearProgressIndicator(minHeight: 2),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _initialScroll.controller,
|
||||
@@ -444,10 +476,7 @@ class _SubtitleColumnState extends State<_SubtitleColumn> {
|
||||
context: context,
|
||||
label: sourceTrack.labelForIndex(trackIndex),
|
||||
isSelected: false,
|
||||
onTap: () {
|
||||
OverlaySheetController.of(context).close();
|
||||
widget.trackControlsState.onSwitchSubtitle!(PlaybackSourceSubtitleChoice.source(sourceTrack.id));
|
||||
},
|
||||
onTap: () => unawaited(_selectSourceSidecar(sourceTrack.id)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ class VideoControlButton extends StatelessWidget {
|
||||
/// If not provided, falls back to tooltip.
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Optional current value announced after [semanticLabel].
|
||||
final String? semanticValue;
|
||||
|
||||
/// Optional checked state for toggle-style controls.
|
||||
final bool? checked;
|
||||
|
||||
/// Whether this button represents an active state (e.g., a feature is enabled).
|
||||
/// When true, the icon color defaults to amber instead of white.
|
||||
final bool isActive;
|
||||
@@ -45,6 +51,8 @@ class VideoControlButton extends StatelessWidget {
|
||||
this.color,
|
||||
this.tooltip,
|
||||
this.semanticLabel,
|
||||
this.semanticValue,
|
||||
this.checked,
|
||||
this.isActive = false,
|
||||
this.focusNode,
|
||||
this.onKeyEvent,
|
||||
@@ -64,9 +72,8 @@ class VideoControlButton extends StatelessWidget {
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
);
|
||||
|
||||
Widget result = semanticLabel != null
|
||||
? Semantics(label: semanticLabel, button: true, excludeSemantics: true, child: button)
|
||||
: button;
|
||||
final effectiveSemanticLabel = semanticLabel ?? tooltip;
|
||||
Widget result = button;
|
||||
|
||||
if (focusNode != null) {
|
||||
result = FocusableWrapper(
|
||||
@@ -75,12 +82,25 @@ class VideoControlButton extends StatelessWidget {
|
||||
onKeyEvent: onKeyEvent,
|
||||
onFocusChange: onFocusChange,
|
||||
autofocus: autofocus,
|
||||
semanticLabel: semanticLabel,
|
||||
semanticLabel: effectiveSemanticLabel,
|
||||
semanticValue: semanticValue,
|
||||
checked: checked,
|
||||
borderRadius: 20, // Circular for icon buttons
|
||||
autoScroll: false, // Video controls don't scroll
|
||||
useBackgroundFocus: true, // Use background highlight for video controls
|
||||
child: result,
|
||||
);
|
||||
} else if (effectiveSemanticLabel != null) {
|
||||
result = Semantics(
|
||||
label: effectiveSemanticLabel,
|
||||
value: semanticValue,
|
||||
button: true,
|
||||
enabled: onPressed != null,
|
||||
checked: checked,
|
||||
onTap: onPressed,
|
||||
excludeSemantics: true,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -61,6 +61,7 @@ import '../../utils/player_utils.dart';
|
||||
import '../../theme/mono_tokens.dart';
|
||||
import '../../utils/provider_extensions.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../utils/latest_async_write.dart';
|
||||
import 'icons.dart';
|
||||
import 'player_chrome_controller.dart';
|
||||
import 'playback_extras_loader.dart';
|
||||
@@ -802,6 +803,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
@override
|
||||
void didUpdateWidget(PlexVideoControls oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.player != widget.player) {
|
||||
++_subtitleVisibilityWriteGeneration;
|
||||
}
|
||||
if (oldWidget.chromeController != widget.chromeController) {
|
||||
oldWidget.chromeController.removeListener(_onChromeChanged);
|
||||
_lastControlsVisible = widget.chromeController.controlsVisible;
|
||||
@@ -827,6 +831,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
++_subtitleVisibilityWriteGeneration;
|
||||
HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
|
||||
widget.chromeController.removeListener(_onChromeChanged);
|
||||
widget.hasFirstFrame?.removeListener(_onFirstFrameReady);
|
||||
@@ -977,7 +982,14 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
const Positioned(top: 0, left: 0, child: LinuxKeepAlive()),
|
||||
// Also handles long-press for 2x speed.
|
||||
Positioned.fill(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: _showControls
|
||||
? t.videoControls.hidePlaybackControls
|
||||
: t.videoControls.showPlaybackControls,
|
||||
onTap: _toggleControlsFromSemantics,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: _handleOuterTap,
|
||||
onLongPressStart: (_) => _handleLongPressStart(),
|
||||
onLongPressEnd: (_) => _handleLongPressEnd(),
|
||||
@@ -986,6 +998,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
child: const ColoredBox(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Mobile double-tap zones for skip forward/backward
|
||||
if (isMobile)
|
||||
MobileSkipZones(
|
||||
|
||||
@@ -27,6 +27,8 @@ class CircularControlButton extends StatelessWidget {
|
||||
child: Semantics(
|
||||
label: semanticLabel,
|
||||
button: true,
|
||||
enabled: isEnabled,
|
||||
onTap: onPressed,
|
||||
excludeSemantics: true,
|
||||
child: IconButton(
|
||||
icon: AppIcon(
|
||||
|
||||
@@ -104,6 +104,18 @@ class ContentStripState extends State<ContentStrip> {
|
||||
if (!identical(oldWidget.player, widget.player) || !identical(oldWidget.chapters, widget.chapters)) {
|
||||
_bindChapterIndexStream();
|
||||
}
|
||||
_normalizeActiveTab();
|
||||
}
|
||||
|
||||
void _normalizeActiveTab() {
|
||||
if (_activeTab == _StripTab.chapters && !_hasChapters && _hasQueue) {
|
||||
_activeTab = _StripTab.queue;
|
||||
_lastAutoScrolledQueueItemID = null;
|
||||
_lastAutoScrolledQueueIndex = null;
|
||||
} else if (_activeTab == _StripTab.queue && !_hasQueue && _hasChapters) {
|
||||
_activeTab = _StripTab.chapters;
|
||||
_lastAutoScrolledChapterIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _bindChapterIndexStream() {
|
||||
|
||||
@@ -69,6 +69,7 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
late double _currentOffset;
|
||||
late double _confirmedOffset;
|
||||
int _writeGeneration = 0;
|
||||
int _bindingGeneration = 0;
|
||||
Timer? _longPressTimer;
|
||||
|
||||
@override
|
||||
@@ -85,6 +86,7 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
widget.player != oldWidget.player ||
|
||||
widget.propertyName != oldWidget.propertyName) {
|
||||
++_writeGeneration;
|
||||
++_bindingGeneration;
|
||||
_currentOffset = widget.initialOffset.toDouble();
|
||||
_confirmedOffset = _currentOffset;
|
||||
}
|
||||
@@ -93,6 +95,7 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
@override
|
||||
void dispose() {
|
||||
++_writeGeneration;
|
||||
++_bindingGeneration;
|
||||
_longPressTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -104,16 +107,26 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
final coordinator = _syncOffsetWrites[targetPlayer] ??= LatestAsyncWrite<String>();
|
||||
final writeToken = coordinator.begin(propertyName);
|
||||
final generation = ++_writeGeneration;
|
||||
final bindingGeneration = _bindingGeneration;
|
||||
unawaited(() async {
|
||||
try {
|
||||
// Convert milliseconds to seconds for mpv.
|
||||
final committed = await coordinator.commitIfLatest(propertyName, writeToken, () async {
|
||||
// Convert milliseconds to seconds for mpv. Keep the native write and
|
||||
// persistence on the same per-player/property queue so an older
|
||||
// native write can never complete after a newer one.
|
||||
final offsetSeconds = offsetMs / 1000.0;
|
||||
await targetPlayer.setProperty(propertyName, offsetSeconds.toString());
|
||||
final committed = await coordinator.commitIfLatest(
|
||||
propertyName,
|
||||
writeToken,
|
||||
() => persistOffset(offsetMs.round()),
|
||||
);
|
||||
await persistOffset(offsetMs.round());
|
||||
if (mounted &&
|
||||
bindingGeneration == _bindingGeneration &&
|
||||
targetPlayer == widget.player &&
|
||||
propertyName == widget.propertyName) {
|
||||
// A write may finish successfully after a newer intent was queued.
|
||||
// Keep it as the rollback baseline without replacing the newer
|
||||
// optimistic value currently shown by the control.
|
||||
_confirmedOffset = offsetMs;
|
||||
}
|
||||
});
|
||||
if (!committed ||
|
||||
!mounted ||
|
||||
generation != _writeGeneration ||
|
||||
@@ -121,7 +134,6 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
propertyName != widget.propertyName) {
|
||||
return;
|
||||
}
|
||||
_confirmedOffset = offsetMs;
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Failed to update playback sync offset', error: error, stackTrace: stackTrace);
|
||||
if (!mounted ||
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../../mpv/mpv.dart';
|
||||
import '../../../media/media_source_info.dart';
|
||||
import '../../../services/sleep_timer_service.dart';
|
||||
import '../../../utils/platform_detector.dart';
|
||||
import '../../../utils/quality_preset_labels.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import '../models/track_controls_state.dart';
|
||||
@@ -18,6 +19,7 @@ import '../sheets/queue_sheet.dart';
|
||||
import '../sheets/track_sheet.dart';
|
||||
import '../sheets/video_settings_sheet.dart';
|
||||
import '../../../services/shader_service.dart';
|
||||
import '../../../utils/track_label_builder.dart';
|
||||
import '../video_control_button.dart';
|
||||
|
||||
/// Row of track and chapter control buttons for the video player
|
||||
@@ -154,12 +156,16 @@ class TrackChapterControls extends StatelessWidget {
|
||||
required bool isMobile,
|
||||
required bool isDesktop,
|
||||
String? tooltip,
|
||||
String? semanticValue,
|
||||
bool? checked,
|
||||
bool isActive = false,
|
||||
}) {
|
||||
return VideoControlButton(
|
||||
icon: icon,
|
||||
tooltip: tooltip,
|
||||
semanticLabel: semanticLabel,
|
||||
semanticValue: semanticValue,
|
||||
checked: checked,
|
||||
isActive: isActive,
|
||||
focusNode: focusNodes != null && focusNodes!.length > buttonIndex ? focusNodes![buttonIndex] : null,
|
||||
onKeyEvent: focusNodes != null
|
||||
@@ -203,7 +209,9 @@ class TrackChapterControls extends StatelessWidget {
|
||||
buttonIndex: 0,
|
||||
icon: Symbols.tune_rounded,
|
||||
isActive: isActive,
|
||||
checked: isActive,
|
||||
tooltip: t.videoControls.settingsButton,
|
||||
semanticValue: _versionQualitySemanticValue(),
|
||||
semanticLabel: t.videoControls.settingsButton,
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
@@ -250,19 +258,25 @@ class TrackChapterControls extends StatelessWidget {
|
||||
// Combined audio & subtitles button
|
||||
{
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
StreamBuilder<TrackSelection>(
|
||||
stream: player.streams.track,
|
||||
initialData: player.state.track,
|
||||
builder: (context, selectionSnapshot) {
|
||||
final selection = selectionSnapshot.data ?? player.state.track;
|
||||
final hasSubtitleControls = trackControlsState.hasSubtitleControls(tracks);
|
||||
final selectedSub = player.state.track.subtitle;
|
||||
final hasActiveSubtitle = selectedSub != null && selectedSub.id != 'no';
|
||||
final selectedSub = selection.subtitle;
|
||||
final hasActiveSubtitle = selectedSub != null && selectedSub.id != SubtitleTrack.off.id;
|
||||
final isHidden = hasSubtitleControls && hasActiveSubtitle && !subtitlesVisible;
|
||||
final icon = hasSubtitleControls
|
||||
? (isHidden ? Symbols.subtitles_off_rounded : Symbols.subtitles_rounded)
|
||||
: Symbols.audiotrack_rounded;
|
||||
buttons.add(
|
||||
_buildTrackButton(
|
||||
return _buildTrackButton(
|
||||
buttonIndex: currentIndex,
|
||||
icon: icon,
|
||||
tooltip: t.videoControls.tracksButton,
|
||||
semanticLabel: t.videoControls.tracksButton,
|
||||
semanticValue: _selectionSemanticValue(tracks, selection),
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: () {
|
||||
@@ -273,6 +287,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
)
|
||||
.whenComplete(() => onStartAutoHide?.call());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
buttonIndex++;
|
||||
@@ -358,6 +374,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
icon: _getBoxFitIcon(boxFitMode),
|
||||
tooltip: _getBoxFitTooltip(boxFitMode),
|
||||
semanticLabel: t.videoControls.aspectRatioButton,
|
||||
semanticValue: _getBoxFitTooltip(boxFitMode),
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: onCycleBoxFitMode,
|
||||
@@ -375,6 +392,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
icon: isRotationLocked ? Symbols.screen_lock_rotation_rounded : Symbols.screen_rotation_rounded,
|
||||
tooltip: isRotationLocked ? t.videoControls.unlockRotation : t.videoControls.lockRotation,
|
||||
semanticLabel: t.videoControls.rotationLockButton,
|
||||
checked: isRotationLocked,
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: onToggleRotationLock,
|
||||
@@ -410,6 +428,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
tooltip: t.videoControls.alwaysOnTopButton,
|
||||
semanticLabel: t.videoControls.alwaysOnTopButton,
|
||||
isActive: isAlwaysOnTop,
|
||||
checked: isAlwaysOnTop,
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: onToggleAlwaysOnTop,
|
||||
@@ -427,6 +446,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
icon: isFullscreen ? Symbols.fullscreen_exit_rounded : Symbols.fullscreen_rounded,
|
||||
tooltip: isFullscreen ? t.videoControls.exitFullscreenButton : t.videoControls.fullscreenButton,
|
||||
semanticLabel: isFullscreen ? t.videoControls.exitFullscreenButton : t.videoControls.fullscreenButton,
|
||||
checked: isFullscreen,
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: onToggleFullscreen,
|
||||
@@ -441,6 +461,62 @@ class TrackChapterControls extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String? _versionQualitySemanticValue() {
|
||||
final values = <String>[];
|
||||
if (availableVersions.length > 1) {
|
||||
final index = selectedMediaIndex;
|
||||
if (index >= 0 && index < availableVersions.length) {
|
||||
values.add(availableVersions[index].displayLabel);
|
||||
}
|
||||
}
|
||||
if (serverSupportsTranscoding) {
|
||||
values.add(qualityPresetLabel(selectedQualityPreset));
|
||||
}
|
||||
return values.isEmpty ? null : values.join(' / ');
|
||||
}
|
||||
|
||||
String? _selectionSemanticValue(Tracks? tracks, TrackSelection selection) {
|
||||
final values = <String>[];
|
||||
final audio = selection.audio;
|
||||
if (audio != null && audio.id != AudioTrack.off.id) {
|
||||
final index = tracks?.audio.indexWhere((track) => track.id == audio.id) ?? -1;
|
||||
final visibleIndex = index < 0 ? 0 : index;
|
||||
final label = TrackLabelBuilder.audioLabel(
|
||||
title: audio.title,
|
||||
language: audio.language,
|
||||
codec: audio.codec,
|
||||
channels: audio.channelsCount,
|
||||
index: visibleIndex,
|
||||
);
|
||||
final fallback = 'Audio Track ${visibleIndex + 1}';
|
||||
values.add(
|
||||
label.primary == fallback
|
||||
? _joinTrackLabel(t.audioTracks.track(n: visibleIndex + 1), label.secondary)
|
||||
: label.joined,
|
||||
);
|
||||
}
|
||||
|
||||
final subtitle = selection.subtitle;
|
||||
if (subtitle != null && subtitle.id != SubtitleTrack.off.id) {
|
||||
final index = tracks?.subtitle.indexWhere((track) => track.id == subtitle.id) ?? -1;
|
||||
values.add(
|
||||
TrackLabelBuilder.subtitleLabel(
|
||||
title: subtitle.title,
|
||||
language: subtitle.language,
|
||||
codec: subtitle.codec,
|
||||
forced: subtitle.isForced,
|
||||
index: index < 0 ? 0 : index,
|
||||
).joined,
|
||||
);
|
||||
}
|
||||
|
||||
return values.isEmpty ? null : values.join(', ');
|
||||
}
|
||||
|
||||
String _joinTrackLabel(String primary, String? secondary) {
|
||||
return secondary == null ? primary : '$primary · $secondary';
|
||||
}
|
||||
|
||||
/// Calculate total button count for navigation
|
||||
int _getButtonCount(bool isMobile, bool isDesktop) {
|
||||
int count = 1; // Settings button always shown
|
||||
|
||||
@@ -48,11 +48,7 @@ class VideoControlsHeader extends StatelessWidget {
|
||||
final itemTitle = metadata.title ?? t.common.unknown;
|
||||
return Row(
|
||||
children: [
|
||||
AppBarBackButton(
|
||||
style: BackButtonStyle.video,
|
||||
semanticLabel: t.common.back,
|
||||
onPressed: onBack ?? () => Navigator.of(context).pop(true),
|
||||
),
|
||||
AppBarBackButton(style: BackButtonStyle.video, onPressed: onBack ?? () => Navigator.of(context).pop(true)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: style == VideoHeaderStyle.singleLine
|
||||
|
||||
@@ -128,6 +128,8 @@ class _VolumeControlState extends State<VolumeControl> {
|
||||
final muteButton = Semantics(
|
||||
label: isMuted ? t.videoControls.unmuteButton : t.videoControls.muteButton,
|
||||
button: true,
|
||||
enabled: true,
|
||||
onTap: widget.volumeController.toggleMute,
|
||||
excludeSemantics: true,
|
||||
child: IconButton(
|
||||
icon: AppIcon(
|
||||
|
||||
@@ -1296,6 +1296,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.7"
|
||||
unorm_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: unorm_dart
|
||||
sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -81,6 +81,7 @@ dependencies:
|
||||
freezed_annotation: ^3.1.0
|
||||
xml: ^6.6.1
|
||||
string_similarity: ^2.2.0
|
||||
unorm_dart: ^0.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user