fix(ui): harden settings focus and semantics

This commit is contained in:
edde746
2026-07-24 03:46:50 +02:00
parent f8bfecf57d
commit b41fb4fe75
159 changed files with 12054 additions and 2050 deletions
+519 -11
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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 -1
View File
@@ -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;
+26 -10
View File
@@ -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,
+3 -1
View File
@@ -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,
+32 -41
View File
@@ -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,
+19 -1
View File
@@ -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) {
+16 -10
View File
@@ -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,
),
),
],
),
);
}
}
+22
View File
@@ -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.
///
+8 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 nest 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "Смотреть в прямом эфире",
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 nest 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 nest 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 lapp — 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "观看直播",
+5 -3
View File
@@ -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) {
+9 -11
View File
@@ -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;
+25 -3
View File
@@ -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();
+7 -2
View File
@@ -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,
)!,
),
+5
View File
@@ -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,
+4
View File
@@ -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) {
+23 -6
View File
@@ -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,
);
}
}
+19 -2
View File
@@ -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> {
),
),
),
),
);
}),
),
),
),
),
);
},
),
+36 -6
View File
@@ -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,
+22 -8
View File
@@ -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,
],
);
},
+5 -2
View File
@@ -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),
+159 -19
View File
@@ -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),
);
}
}
+97 -50
View File
@@ -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);
}
}
+35 -1
View File
@@ -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({
+31 -11
View File
@@ -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;
}
}
+50 -11
View File
@@ -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;
+54 -31
View File
@@ -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);
+16 -15
View File
@@ -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,
+123 -33
View File
@@ -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();
}
}
+1 -2
View File
@@ -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
+82 -56
View File
@@ -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});
+32 -8
View File
@@ -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;
}
}
+65 -13
View File
@@ -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;
+1 -5
View File
@@ -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());
}
}
+9
View File
@@ -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 {
),
),
),
),
);
}
+1
View File
@@ -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(
+5
View File
@@ -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,
+2
View File
@@ -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,
+1
View File
@@ -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;
+7 -5
View File
@@ -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
View File
@@ -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,
+104 -23
View File
@@ -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();
}
+69 -26
View File
@@ -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}) {
+12 -12
View File
@@ -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,
+7 -9
View File
@@ -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;
+5 -3
View File
@@ -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;
+13 -28
View File
@@ -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(
+8
View File
@@ -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:
+1
View File
@@ -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