From dc443540360f0bdd179a2141458d0ed628c5974c Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:15:06 +0100 Subject: [PATCH] feat: downloads --- ios/Podfile.lock | 102 + ios/Runner.xcodeproj/project.pbxproj | 3 + ios/Runner/Info.plist | 4 + lib/database/app_database.dart | 208 + lib/database/app_database.g.dart | 3635 +++ lib/database/tables/api_cache.dart | 20 + lib/database/tables/download_queue.dart | 13 + lib/database/tables/downloaded_media.dart | 21 + .../tables/offline_watch_progress.dart | 46 + lib/i18n/strings.g.dart | 20593 +++++++++++----- lib/i18n/strings.i18n.json | 75 +- lib/i18n/strings_de.i18n.json | 77 +- lib/i18n/strings_it.i18n.json | 75 +- lib/i18n/strings_nl.i18n.json | 75 +- lib/i18n/strings_sv.i18n.json | 75 +- lib/i18n/strings_zh.i18n.json | 75 +- lib/main.dart | 138 +- lib/models/deletion_progress.dart | 45 + lib/models/download_progress.dart | 81 + lib/models/download_status.dart | 9 + lib/models/plex_media_info.dart | 8 + lib/models/plex_metadata.dart | 37 +- lib/models/plex_metadata.g.dart | 2 + lib/providers/download_provider.dart | 1277 + lib/providers/offline_mode_provider.dart | 83 + lib/providers/offline_watch_provider.dart | 214 + lib/screens/downloads/downloads_screen.dart | 365 + lib/screens/main_screen.dart | 250 +- lib/screens/media_detail_screen.dart | 1032 +- lib/screens/season_detail_screen.dart | 302 +- lib/screens/settings/settings_screen.dart | 186 + lib/screens/video_player_screen.dart | 111 +- lib/services/download_manager_service.dart | 1717 ++ lib/services/download_storage_service.dart | 710 + lib/services/offline_watch_sync_service.dart | 507 + .../playback_initialization_service.dart | 129 +- lib/services/playback_progress_tracker.dart | 115 +- lib/services/plex_api_cache.dart | 142 + lib/services/plex_client.dart | 349 +- lib/services/saf_storage_service.dart | 233 + lib/services/settings_service.dart | 41 + lib/utils/plex_image_helper.dart | 9 +- lib/utils/smart_deletion_handler.dart | 79 + lib/utils/video_player_navigation.dart | 3 + lib/widgets/deletion_progress_dialog.dart | 80 + lib/widgets/download_tree_item.dart | 363 + lib/widgets/download_tree_view.dart | 639 + lib/widgets/focusable_media_card.dart | 5 + lib/widgets/hub_section.dart | 10 +- lib/widgets/media_card.dart | 69 +- lib/widgets/media_context_menu.dart | 199 +- lib/widgets/plex_optimized_image.dart | 48 +- lib/widgets/side_navigation_rail.dart | 100 +- .../video_controls/sheets/chapter_sheet.dart | 24 +- .../video_controls/video_controls.dart | 116 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + macos/Podfile.lock | 43 + macos/Runner/Configs/AppInfo.xcconfig | 4 +- pubspec.lock | 130 +- pubspec.yaml | 8 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/Runner.rc | 4 +- 65 files changed, 28245 insertions(+), 6883 deletions(-) create mode 100644 lib/database/app_database.dart create mode 100644 lib/database/app_database.g.dart create mode 100644 lib/database/tables/api_cache.dart create mode 100644 lib/database/tables/download_queue.dart create mode 100644 lib/database/tables/downloaded_media.dart create mode 100644 lib/database/tables/offline_watch_progress.dart create mode 100644 lib/models/deletion_progress.dart create mode 100644 lib/models/download_progress.dart create mode 100644 lib/models/download_status.dart create mode 100644 lib/providers/download_provider.dart create mode 100644 lib/providers/offline_mode_provider.dart create mode 100644 lib/providers/offline_watch_provider.dart create mode 100644 lib/screens/downloads/downloads_screen.dart create mode 100644 lib/services/download_manager_service.dart create mode 100644 lib/services/download_storage_service.dart create mode 100644 lib/services/offline_watch_sync_service.dart create mode 100644 lib/services/plex_api_cache.dart create mode 100644 lib/services/saf_storage_service.dart create mode 100644 lib/utils/smart_deletion_handler.dart create mode 100644 lib/widgets/deletion_progress_dialog.dart create mode 100644 lib/widgets/download_tree_item.dart create mode 100644 lib/widgets/download_tree_view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e1eee8e7..1ee582a1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,7 +3,43 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Flutter (1.0.0) + - gamepads_ios (0.1.1): + - Flutter - os_media_controls (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -11,33 +47,83 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - SDWebImage (5.21.3): + - SDWebImage/Core (= 5.21.3) + - SDWebImage/Core (5.21.3) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - sqlite3 (3.51.1): + - sqlite3/common (= 3.51.1) + - sqlite3/common (3.51.1) + - sqlite3/dbstatvtab (3.51.1): + - sqlite3/common + - sqlite3/fts5 (3.51.1): + - sqlite3/common + - sqlite3/math (3.51.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.51.1): + - sqlite3/common + - sqlite3/rtree (3.51.1): + - sqlite3/common + - sqlite3/session (3.51.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.51.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - sqlite3/session + - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter + - wakelock_plus (0.0.1): + - Flutter + - workmanager_apple (0.0.1): + - Flutter DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`) - os_media_controls (from `.symlinks/plugins/os_media_controls/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - SDWebImage + - sqlite3 + - SwiftyGif EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + gamepads_ios: + :path: ".symlinks/plugins/gamepads_ios/ios" os_media_controls: :path: ".symlinks/plugins/os_media_controls/ios" package_info_plus: @@ -48,19 +134,35 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + workmanager_apple: + :path: ".symlinks/plugins/workmanager_apple/ios" SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + gamepads_ios: c75c6d31377d275b0effb9174c619e2705678c09 os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b + sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9dab84dd..65cff4e7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -496,6 +496,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -682,6 +683,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -708,6 +710,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 0fc3e407..1d395ccc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -39,6 +41,8 @@ This app needs to connect to your Plex Media Server on your local network. UIApplicationSupportsIndirectInputEvents + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart new file mode 100644 index 00000000..1f247c53 --- /dev/null +++ b/lib/database/app_database.dart @@ -0,0 +1,208 @@ +import 'dart:io'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +import 'tables/downloaded_media.dart'; +import 'tables/download_queue.dart'; +import 'tables/api_cache.dart'; +import 'tables/offline_watch_progress.dart'; +import '../models/download_status.dart'; +import '../utils/app_logger.dart'; + +part 'app_database.g.dart'; + +// Simplified database with API cache for offline support +@DriftDatabase( + tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress], +) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 7; // Added OfflineWatchProgress table + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: (Migrator m, int from, int to) async { + // Additive migration for schema version 7 + if (from < 7) { + appLogger.i('Adding OfflineWatchProgress table (v7 migration)'); + await m.createTable(offlineWatchProgress); + } + }, + ); + } + + // ============================================================ + // Offline Watch Progress Operations + // ============================================================ + + /// Get all pending offline watch actions for sync + Future> getPendingWatchActions() { + return (select( + offlineWatchProgress, + )..orderBy([(t) => OrderingTerm.asc(t.createdAt)])).get(); + } + + /// Get pending watch actions for a specific server + Future> getPendingWatchActionsForServer( + String serverId, + ) { + return (select(offlineWatchProgress) + ..where((t) => t.serverId.equals(serverId)) + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + .get(); + } + + /// Get the latest action for a specific item + Future getLatestWatchAction(String globalKey) { + return (select(offlineWatchProgress) + ..where((t) => t.globalKey.equals(globalKey)) + ..orderBy([(t) => OrderingTerm.desc(t.updatedAt)]) + ..limit(1)) + .getSingleOrNull(); + } + + /// Insert or update a progress action (merges with existing) + Future upsertProgressAction({ + required String serverId, + required String ratingKey, + required int viewOffset, + required int duration, + required bool shouldMarkWatched, + }) async { + final globalKey = '$serverId:$ratingKey'; + final now = DateTime.now().millisecondsSinceEpoch; + + // Check for existing progress entry + final existing = + await (select(offlineWatchProgress) + ..where( + (t) => + t.globalKey.equals(globalKey) & + t.actionType.equals('progress'), + ) + ..limit(1)) + .getSingleOrNull(); + + if (existing != null) { + // Update existing progress entry + await (update( + offlineWatchProgress, + )..where((t) => t.id.equals(existing.id))).write( + OfflineWatchProgressCompanion( + viewOffset: Value(viewOffset), + duration: Value(duration), + shouldMarkWatched: Value(shouldMarkWatched), + updatedAt: Value(now), + ), + ); + } else { + // Insert new progress entry + await into(offlineWatchProgress).insert( + OfflineWatchProgressCompanion.insert( + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + actionType: 'progress', + viewOffset: Value(viewOffset), + duration: Value(duration), + shouldMarkWatched: Value(shouldMarkWatched), + createdAt: now, + updatedAt: now, + ), + ); + } + } + + /// Insert a manual watch action (watched or unwatched) + /// Removes conflicting actions for the same item + Future insertWatchAction({ + required String serverId, + required String ratingKey, + required String actionType, // 'watched' or 'unwatched' + }) async { + final globalKey = '$serverId:$ratingKey'; + final now = DateTime.now().millisecondsSinceEpoch; + + // Remove conflicting actions (opposite action type and progress) + await (delete( + offlineWatchProgress, + )..where((t) => t.globalKey.equals(globalKey))).go(); + + // Insert the new action + await into(offlineWatchProgress).insert( + OfflineWatchProgressCompanion.insert( + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + actionType: actionType, + createdAt: now, + updatedAt: now, + ), + ); + } + + /// Delete a specific watch action after successful sync + Future deleteWatchAction(int id) { + return (delete(offlineWatchProgress)..where((t) => t.id.equals(id))).go(); + } + + /// Update sync attempt count and error message + Future updateSyncAttempt(int id, String? errorMessage) async { + final existing = await (select( + offlineWatchProgress, + )..where((t) => t.id.equals(id))).getSingleOrNull(); + + if (existing != null) { + await (update(offlineWatchProgress)..where((t) => t.id.equals(id))).write( + OfflineWatchProgressCompanion( + syncAttempts: Value(existing.syncAttempts + 1), + lastError: Value(errorMessage), + ), + ); + } + } + + /// Get count of pending sync items + Future getPendingSyncCount() async { + final count = + await (selectOnly(offlineWatchProgress) + ..addColumns([offlineWatchProgress.id.count()])) + .map((row) => row.read(offlineWatchProgress.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Clear all pending watch actions (e.g., after logout) + Future clearAllWatchActions() { + return delete(offlineWatchProgress).go(); + } + + // ============================================================ + // Downloaded Media Queries for Watch State Sync + // ============================================================ + + /// Get all downloaded media items (for syncing watch states) + Future> getAllDownloadedMetadata() { + return (select(downloadedMedia) + ..where( + (t) => t.status.equals(DownloadStatus.completed.index), + )) + .get(); + } +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'plezy_downloads.db')); + return NativeDatabase(file); + }); +} diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart new file mode 100644 index 00000000..db269630 --- /dev/null +++ b/lib/database/app_database.g.dart @@ -0,0 +1,3635 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_database.dart'; + +// ignore_for_file: type=lint +class $DownloadedMediaTable extends DownloadedMedia + with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $DownloadedMediaTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); + @override + late final GeneratedColumn ratingKey = GeneratedColumn( + 'rating_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); + @override + late final GeneratedColumn globalKey = GeneratedColumn( + 'global_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta( + 'parentRatingKey', + ); + @override + late final GeneratedColumn parentRatingKey = GeneratedColumn( + 'parent_rating_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _grandparentRatingKeyMeta = + const VerificationMeta('grandparentRatingKey'); + @override + late final GeneratedColumn grandparentRatingKey = + GeneratedColumn( + 'grandparent_rating_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _progressMeta = const VerificationMeta( + 'progress', + ); + @override + late final GeneratedColumn progress = GeneratedColumn( + 'progress', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _totalBytesMeta = const VerificationMeta( + 'totalBytes', + ); + @override + late final GeneratedColumn totalBytes = GeneratedColumn( + 'total_bytes', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _downloadedBytesMeta = const VerificationMeta( + 'downloadedBytes', + ); + @override + late final GeneratedColumn downloadedBytes = GeneratedColumn( + 'downloaded_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _videoFilePathMeta = const VerificationMeta( + 'videoFilePath', + ); + @override + late final GeneratedColumn videoFilePath = GeneratedColumn( + 'video_file_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _thumbPathMeta = const VerificationMeta( + 'thumbPath', + ); + @override + late final GeneratedColumn thumbPath = GeneratedColumn( + 'thumb_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _downloadedAtMeta = const VerificationMeta( + 'downloadedAt', + ); + @override + late final GeneratedColumn downloadedAt = GeneratedColumn( + 'downloaded_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _errorMessageMeta = const VerificationMeta( + 'errorMessage', + ); + @override + late final GeneratedColumn errorMessage = GeneratedColumn( + 'error_message', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _retryCountMeta = const VerificationMeta( + 'retryCount', + ); + @override + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + @override + List get $columns => [ + id, + serverId, + ratingKey, + globalKey, + type, + parentRatingKey, + grandparentRatingKey, + status, + progress, + totalBytes, + downloadedBytes, + videoFilePath, + thumbPath, + downloadedAt, + errorMessage, + retryCount, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'downloaded_media'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('rating_key')) { + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); + } else if (isInserting) { + context.missing(_ratingKeyMeta); + } + if (data.containsKey('global_key')) { + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); + } else if (isInserting) { + context.missing(_globalKeyMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('parent_rating_key')) { + context.handle( + _parentRatingKeyMeta, + parentRatingKey.isAcceptableOrUnknown( + data['parent_rating_key']!, + _parentRatingKeyMeta, + ), + ); + } + if (data.containsKey('grandparent_rating_key')) { + context.handle( + _grandparentRatingKeyMeta, + grandparentRatingKey.isAcceptableOrUnknown( + data['grandparent_rating_key']!, + _grandparentRatingKeyMeta, + ), + ); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } else if (isInserting) { + context.missing(_statusMeta); + } + if (data.containsKey('progress')) { + context.handle( + _progressMeta, + progress.isAcceptableOrUnknown(data['progress']!, _progressMeta), + ); + } + if (data.containsKey('total_bytes')) { + context.handle( + _totalBytesMeta, + totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta), + ); + } + if (data.containsKey('downloaded_bytes')) { + context.handle( + _downloadedBytesMeta, + downloadedBytes.isAcceptableOrUnknown( + data['downloaded_bytes']!, + _downloadedBytesMeta, + ), + ); + } + if (data.containsKey('video_file_path')) { + context.handle( + _videoFilePathMeta, + videoFilePath.isAcceptableOrUnknown( + data['video_file_path']!, + _videoFilePathMeta, + ), + ); + } + if (data.containsKey('thumb_path')) { + context.handle( + _thumbPathMeta, + thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta), + ); + } + if (data.containsKey('downloaded_at')) { + context.handle( + _downloadedAtMeta, + downloadedAt.isAcceptableOrUnknown( + data['downloaded_at']!, + _downloadedAtMeta, + ), + ); + } + if (data.containsKey('error_message')) { + context.handle( + _errorMessageMeta, + errorMessage.isAcceptableOrUnknown( + data['error_message']!, + _errorMessageMeta, + ), + ); + } + if (data.containsKey('retry_count')) { + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + DownloadedMediaItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DownloadedMediaItem( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + parentRatingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}parent_rating_key'], + ), + grandparentRatingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}grandparent_rating_key'], + ), + status: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}status'], + )!, + progress: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}progress'], + )!, + totalBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_bytes'], + ), + downloadedBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_bytes'], + )!, + videoFilePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}video_file_path'], + ), + thumbPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_path'], + ), + downloadedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_at'], + ), + errorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_message'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, + ); + } + + @override + $DownloadedMediaTable createAlias(String alias) { + return $DownloadedMediaTable(attachedDatabase, alias); + } +} + +class DownloadedMediaItem extends DataClass + implements Insertable { + final int id; + final String serverId; + final String ratingKey; + final String globalKey; + final String type; + final String? parentRatingKey; + final String? grandparentRatingKey; + final int status; + final int progress; + final int? totalBytes; + final int downloadedBytes; + final String? videoFilePath; + final String? thumbPath; + final int? downloadedAt; + final String? errorMessage; + final int retryCount; + const DownloadedMediaItem({ + required this.id, + required this.serverId, + required this.ratingKey, + required this.globalKey, + required this.type, + this.parentRatingKey, + this.grandparentRatingKey, + required this.status, + required this.progress, + this.totalBytes, + required this.downloadedBytes, + this.videoFilePath, + this.thumbPath, + this.downloadedAt, + this.errorMessage, + required this.retryCount, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['server_id'] = Variable(serverId); + map['rating_key'] = Variable(ratingKey); + map['global_key'] = Variable(globalKey); + map['type'] = Variable(type); + if (!nullToAbsent || parentRatingKey != null) { + map['parent_rating_key'] = Variable(parentRatingKey); + } + if (!nullToAbsent || grandparentRatingKey != null) { + map['grandparent_rating_key'] = Variable(grandparentRatingKey); + } + map['status'] = Variable(status); + map['progress'] = Variable(progress); + if (!nullToAbsent || totalBytes != null) { + map['total_bytes'] = Variable(totalBytes); + } + map['downloaded_bytes'] = Variable(downloadedBytes); + if (!nullToAbsent || videoFilePath != null) { + map['video_file_path'] = Variable(videoFilePath); + } + if (!nullToAbsent || thumbPath != null) { + map['thumb_path'] = Variable(thumbPath); + } + if (!nullToAbsent || downloadedAt != null) { + map['downloaded_at'] = Variable(downloadedAt); + } + if (!nullToAbsent || errorMessage != null) { + map['error_message'] = Variable(errorMessage); + } + map['retry_count'] = Variable(retryCount); + return map; + } + + DownloadedMediaCompanion toCompanion(bool nullToAbsent) { + return DownloadedMediaCompanion( + id: Value(id), + serverId: Value(serverId), + ratingKey: Value(ratingKey), + globalKey: Value(globalKey), + type: Value(type), + parentRatingKey: parentRatingKey == null && nullToAbsent + ? const Value.absent() + : Value(parentRatingKey), + grandparentRatingKey: grandparentRatingKey == null && nullToAbsent + ? const Value.absent() + : Value(grandparentRatingKey), + status: Value(status), + progress: Value(progress), + totalBytes: totalBytes == null && nullToAbsent + ? const Value.absent() + : Value(totalBytes), + downloadedBytes: Value(downloadedBytes), + videoFilePath: videoFilePath == null && nullToAbsent + ? const Value.absent() + : Value(videoFilePath), + thumbPath: thumbPath == null && nullToAbsent + ? const Value.absent() + : Value(thumbPath), + downloadedAt: downloadedAt == null && nullToAbsent + ? const Value.absent() + : Value(downloadedAt), + errorMessage: errorMessage == null && nullToAbsent + ? const Value.absent() + : Value(errorMessage), + retryCount: Value(retryCount), + ); + } + + factory DownloadedMediaItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DownloadedMediaItem( + id: serializer.fromJson(json['id']), + serverId: serializer.fromJson(json['serverId']), + ratingKey: serializer.fromJson(json['ratingKey']), + globalKey: serializer.fromJson(json['globalKey']), + type: serializer.fromJson(json['type']), + parentRatingKey: serializer.fromJson(json['parentRatingKey']), + grandparentRatingKey: serializer.fromJson( + json['grandparentRatingKey'], + ), + status: serializer.fromJson(json['status']), + progress: serializer.fromJson(json['progress']), + totalBytes: serializer.fromJson(json['totalBytes']), + downloadedBytes: serializer.fromJson(json['downloadedBytes']), + videoFilePath: serializer.fromJson(json['videoFilePath']), + thumbPath: serializer.fromJson(json['thumbPath']), + downloadedAt: serializer.fromJson(json['downloadedAt']), + errorMessage: serializer.fromJson(json['errorMessage']), + retryCount: serializer.fromJson(json['retryCount']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'serverId': serializer.toJson(serverId), + 'ratingKey': serializer.toJson(ratingKey), + 'globalKey': serializer.toJson(globalKey), + 'type': serializer.toJson(type), + 'parentRatingKey': serializer.toJson(parentRatingKey), + 'grandparentRatingKey': serializer.toJson(grandparentRatingKey), + 'status': serializer.toJson(status), + 'progress': serializer.toJson(progress), + 'totalBytes': serializer.toJson(totalBytes), + 'downloadedBytes': serializer.toJson(downloadedBytes), + 'videoFilePath': serializer.toJson(videoFilePath), + 'thumbPath': serializer.toJson(thumbPath), + 'downloadedAt': serializer.toJson(downloadedAt), + 'errorMessage': serializer.toJson(errorMessage), + 'retryCount': serializer.toJson(retryCount), + }; + } + + DownloadedMediaItem copyWith({ + int? id, + String? serverId, + String? ratingKey, + String? globalKey, + String? type, + Value parentRatingKey = const Value.absent(), + Value grandparentRatingKey = const Value.absent(), + int? status, + int? progress, + Value totalBytes = const Value.absent(), + int? downloadedBytes, + Value videoFilePath = const Value.absent(), + Value thumbPath = const Value.absent(), + Value downloadedAt = const Value.absent(), + Value errorMessage = const Value.absent(), + int? retryCount, + }) => DownloadedMediaItem( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + ratingKey: ratingKey ?? this.ratingKey, + globalKey: globalKey ?? this.globalKey, + type: type ?? this.type, + parentRatingKey: parentRatingKey.present + ? parentRatingKey.value + : this.parentRatingKey, + grandparentRatingKey: grandparentRatingKey.present + ? grandparentRatingKey.value + : this.grandparentRatingKey, + status: status ?? this.status, + progress: progress ?? this.progress, + totalBytes: totalBytes.present ? totalBytes.value : this.totalBytes, + downloadedBytes: downloadedBytes ?? this.downloadedBytes, + videoFilePath: videoFilePath.present + ? videoFilePath.value + : this.videoFilePath, + thumbPath: thumbPath.present ? thumbPath.value : this.thumbPath, + downloadedAt: downloadedAt.present ? downloadedAt.value : this.downloadedAt, + errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage, + retryCount: retryCount ?? this.retryCount, + ); + DownloadedMediaItem copyWithCompanion(DownloadedMediaCompanion data) { + return DownloadedMediaItem( + id: data.id.present ? data.id.value : this.id, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + ratingKey: data.ratingKey.present ? data.ratingKey.value : this.ratingKey, + globalKey: data.globalKey.present ? data.globalKey.value : this.globalKey, + type: data.type.present ? data.type.value : this.type, + parentRatingKey: data.parentRatingKey.present + ? data.parentRatingKey.value + : this.parentRatingKey, + grandparentRatingKey: data.grandparentRatingKey.present + ? data.grandparentRatingKey.value + : this.grandparentRatingKey, + status: data.status.present ? data.status.value : this.status, + progress: data.progress.present ? data.progress.value : this.progress, + totalBytes: data.totalBytes.present + ? data.totalBytes.value + : this.totalBytes, + downloadedBytes: data.downloadedBytes.present + ? data.downloadedBytes.value + : this.downloadedBytes, + videoFilePath: data.videoFilePath.present + ? data.videoFilePath.value + : this.videoFilePath, + thumbPath: data.thumbPath.present ? data.thumbPath.value : this.thumbPath, + downloadedAt: data.downloadedAt.present + ? data.downloadedAt.value + : this.downloadedAt, + errorMessage: data.errorMessage.present + ? data.errorMessage.value + : this.errorMessage, + retryCount: data.retryCount.present + ? data.retryCount.value + : this.retryCount, + ); + } + + @override + String toString() { + return (StringBuffer('DownloadedMediaItem(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('ratingKey: $ratingKey, ') + ..write('globalKey: $globalKey, ') + ..write('type: $type, ') + ..write('parentRatingKey: $parentRatingKey, ') + ..write('grandparentRatingKey: $grandparentRatingKey, ') + ..write('status: $status, ') + ..write('progress: $progress, ') + ..write('totalBytes: $totalBytes, ') + ..write('downloadedBytes: $downloadedBytes, ') + ..write('videoFilePath: $videoFilePath, ') + ..write('thumbPath: $thumbPath, ') + ..write('downloadedAt: $downloadedAt, ') + ..write('errorMessage: $errorMessage, ') + ..write('retryCount: $retryCount') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + serverId, + ratingKey, + globalKey, + type, + parentRatingKey, + grandparentRatingKey, + status, + progress, + totalBytes, + downloadedBytes, + videoFilePath, + thumbPath, + downloadedAt, + errorMessage, + retryCount, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DownloadedMediaItem && + other.id == this.id && + other.serverId == this.serverId && + other.ratingKey == this.ratingKey && + other.globalKey == this.globalKey && + other.type == this.type && + other.parentRatingKey == this.parentRatingKey && + other.grandparentRatingKey == this.grandparentRatingKey && + other.status == this.status && + other.progress == this.progress && + other.totalBytes == this.totalBytes && + other.downloadedBytes == this.downloadedBytes && + other.videoFilePath == this.videoFilePath && + other.thumbPath == this.thumbPath && + other.downloadedAt == this.downloadedAt && + other.errorMessage == this.errorMessage && + other.retryCount == this.retryCount); +} + +class DownloadedMediaCompanion extends UpdateCompanion { + final Value id; + final Value serverId; + final Value ratingKey; + final Value globalKey; + final Value type; + final Value parentRatingKey; + final Value grandparentRatingKey; + final Value status; + final Value progress; + final Value totalBytes; + final Value downloadedBytes; + final Value videoFilePath; + final Value thumbPath; + final Value downloadedAt; + final Value errorMessage; + final Value retryCount; + const DownloadedMediaCompanion({ + this.id = const Value.absent(), + this.serverId = const Value.absent(), + this.ratingKey = const Value.absent(), + this.globalKey = const Value.absent(), + this.type = const Value.absent(), + this.parentRatingKey = const Value.absent(), + this.grandparentRatingKey = const Value.absent(), + this.status = const Value.absent(), + this.progress = const Value.absent(), + this.totalBytes = const Value.absent(), + this.downloadedBytes = const Value.absent(), + this.videoFilePath = const Value.absent(), + this.thumbPath = const Value.absent(), + this.downloadedAt = const Value.absent(), + this.errorMessage = const Value.absent(), + this.retryCount = const Value.absent(), + }); + DownloadedMediaCompanion.insert({ + this.id = const Value.absent(), + required String serverId, + required String ratingKey, + required String globalKey, + required String type, + this.parentRatingKey = const Value.absent(), + this.grandparentRatingKey = const Value.absent(), + required int status, + this.progress = const Value.absent(), + this.totalBytes = const Value.absent(), + this.downloadedBytes = const Value.absent(), + this.videoFilePath = const Value.absent(), + this.thumbPath = const Value.absent(), + this.downloadedAt = const Value.absent(), + this.errorMessage = const Value.absent(), + this.retryCount = const Value.absent(), + }) : serverId = Value(serverId), + ratingKey = Value(ratingKey), + globalKey = Value(globalKey), + type = Value(type), + status = Value(status); + static Insertable custom({ + Expression? id, + Expression? serverId, + Expression? ratingKey, + Expression? globalKey, + Expression? type, + Expression? parentRatingKey, + Expression? grandparentRatingKey, + Expression? status, + Expression? progress, + Expression? totalBytes, + Expression? downloadedBytes, + Expression? videoFilePath, + Expression? thumbPath, + Expression? downloadedAt, + Expression? errorMessage, + Expression? retryCount, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (serverId != null) 'server_id': serverId, + if (ratingKey != null) 'rating_key': ratingKey, + if (globalKey != null) 'global_key': globalKey, + if (type != null) 'type': type, + if (parentRatingKey != null) 'parent_rating_key': parentRatingKey, + if (grandparentRatingKey != null) + 'grandparent_rating_key': grandparentRatingKey, + if (status != null) 'status': status, + if (progress != null) 'progress': progress, + if (totalBytes != null) 'total_bytes': totalBytes, + if (downloadedBytes != null) 'downloaded_bytes': downloadedBytes, + if (videoFilePath != null) 'video_file_path': videoFilePath, + if (thumbPath != null) 'thumb_path': thumbPath, + if (downloadedAt != null) 'downloaded_at': downloadedAt, + if (errorMessage != null) 'error_message': errorMessage, + if (retryCount != null) 'retry_count': retryCount, + }); + } + + DownloadedMediaCompanion copyWith({ + Value? id, + Value? serverId, + Value? ratingKey, + Value? globalKey, + Value? type, + Value? parentRatingKey, + Value? grandparentRatingKey, + Value? status, + Value? progress, + Value? totalBytes, + Value? downloadedBytes, + Value? videoFilePath, + Value? thumbPath, + Value? downloadedAt, + Value? errorMessage, + Value? retryCount, + }) { + return DownloadedMediaCompanion( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + ratingKey: ratingKey ?? this.ratingKey, + globalKey: globalKey ?? this.globalKey, + type: type ?? this.type, + parentRatingKey: parentRatingKey ?? this.parentRatingKey, + grandparentRatingKey: grandparentRatingKey ?? this.grandparentRatingKey, + status: status ?? this.status, + progress: progress ?? this.progress, + totalBytes: totalBytes ?? this.totalBytes, + downloadedBytes: downloadedBytes ?? this.downloadedBytes, + videoFilePath: videoFilePath ?? this.videoFilePath, + thumbPath: thumbPath ?? this.thumbPath, + downloadedAt: downloadedAt ?? this.downloadedAt, + errorMessage: errorMessage ?? this.errorMessage, + retryCount: retryCount ?? this.retryCount, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (ratingKey.present) { + map['rating_key'] = Variable(ratingKey.value); + } + if (globalKey.present) { + map['global_key'] = Variable(globalKey.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (parentRatingKey.present) { + map['parent_rating_key'] = Variable(parentRatingKey.value); + } + if (grandparentRatingKey.present) { + map['grandparent_rating_key'] = Variable( + grandparentRatingKey.value, + ); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (progress.present) { + map['progress'] = Variable(progress.value); + } + if (totalBytes.present) { + map['total_bytes'] = Variable(totalBytes.value); + } + if (downloadedBytes.present) { + map['downloaded_bytes'] = Variable(downloadedBytes.value); + } + if (videoFilePath.present) { + map['video_file_path'] = Variable(videoFilePath.value); + } + if (thumbPath.present) { + map['thumb_path'] = Variable(thumbPath.value); + } + if (downloadedAt.present) { + map['downloaded_at'] = Variable(downloadedAt.value); + } + if (errorMessage.present) { + map['error_message'] = Variable(errorMessage.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DownloadedMediaCompanion(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('ratingKey: $ratingKey, ') + ..write('globalKey: $globalKey, ') + ..write('type: $type, ') + ..write('parentRatingKey: $parentRatingKey, ') + ..write('grandparentRatingKey: $grandparentRatingKey, ') + ..write('status: $status, ') + ..write('progress: $progress, ') + ..write('totalBytes: $totalBytes, ') + ..write('downloadedBytes: $downloadedBytes, ') + ..write('videoFilePath: $videoFilePath, ') + ..write('thumbPath: $thumbPath, ') + ..write('downloadedAt: $downloadedAt, ') + ..write('errorMessage: $errorMessage, ') + ..write('retryCount: $retryCount') + ..write(')')) + .toString(); + } +} + +class $DownloadQueueTable extends DownloadQueue + with TableInfo<$DownloadQueueTable, DownloadQueueItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $DownloadQueueTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta( + 'mediaGlobalKey', + ); + @override + late final GeneratedColumn mediaGlobalKey = GeneratedColumn( + 'media_global_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _priorityMeta = const VerificationMeta( + 'priority', + ); + @override + late final GeneratedColumn priority = GeneratedColumn( + 'priority', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _addedAtMeta = const VerificationMeta( + 'addedAt', + ); + @override + late final GeneratedColumn addedAt = GeneratedColumn( + 'added_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta( + 'downloadSubtitles', + ); + @override + late final GeneratedColumn downloadSubtitles = GeneratedColumn( + 'download_subtitles', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_subtitles" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _downloadArtworkMeta = const VerificationMeta( + 'downloadArtwork', + ); + @override + late final GeneratedColumn downloadArtwork = GeneratedColumn( + 'download_artwork', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_artwork" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + @override + List get $columns => [ + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'download_queue'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('media_global_key')) { + context.handle( + _mediaGlobalKeyMeta, + mediaGlobalKey.isAcceptableOrUnknown( + data['media_global_key']!, + _mediaGlobalKeyMeta, + ), + ); + } else if (isInserting) { + context.missing(_mediaGlobalKeyMeta); + } + if (data.containsKey('priority')) { + context.handle( + _priorityMeta, + priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta), + ); + } + if (data.containsKey('added_at')) { + context.handle( + _addedAtMeta, + addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta), + ); + } else if (isInserting) { + context.missing(_addedAtMeta); + } + if (data.containsKey('download_subtitles')) { + context.handle( + _downloadSubtitlesMeta, + downloadSubtitles.isAcceptableOrUnknown( + data['download_subtitles']!, + _downloadSubtitlesMeta, + ), + ); + } + if (data.containsKey('download_artwork')) { + context.handle( + _downloadArtworkMeta, + downloadArtwork.isAcceptableOrUnknown( + data['download_artwork']!, + _downloadArtworkMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + DownloadQueueItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DownloadQueueItem( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + mediaGlobalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_global_key'], + )!, + priority: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}priority'], + )!, + addedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}added_at'], + )!, + downloadSubtitles: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}download_subtitles'], + )!, + downloadArtwork: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}download_artwork'], + )!, + ); + } + + @override + $DownloadQueueTable createAlias(String alias) { + return $DownloadQueueTable(attachedDatabase, alias); + } +} + +class DownloadQueueItem extends DataClass + implements Insertable { + final int id; + final String mediaGlobalKey; + final int priority; + final int addedAt; + final bool downloadSubtitles; + final bool downloadArtwork; + const DownloadQueueItem({ + required this.id, + required this.mediaGlobalKey, + required this.priority, + required this.addedAt, + required this.downloadSubtitles, + required this.downloadArtwork, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['media_global_key'] = Variable(mediaGlobalKey); + map['priority'] = Variable(priority); + map['added_at'] = Variable(addedAt); + map['download_subtitles'] = Variable(downloadSubtitles); + map['download_artwork'] = Variable(downloadArtwork); + return map; + } + + DownloadQueueCompanion toCompanion(bool nullToAbsent) { + return DownloadQueueCompanion( + id: Value(id), + mediaGlobalKey: Value(mediaGlobalKey), + priority: Value(priority), + addedAt: Value(addedAt), + downloadSubtitles: Value(downloadSubtitles), + downloadArtwork: Value(downloadArtwork), + ); + } + + factory DownloadQueueItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DownloadQueueItem( + id: serializer.fromJson(json['id']), + mediaGlobalKey: serializer.fromJson(json['mediaGlobalKey']), + priority: serializer.fromJson(json['priority']), + addedAt: serializer.fromJson(json['addedAt']), + downloadSubtitles: serializer.fromJson(json['downloadSubtitles']), + downloadArtwork: serializer.fromJson(json['downloadArtwork']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'mediaGlobalKey': serializer.toJson(mediaGlobalKey), + 'priority': serializer.toJson(priority), + 'addedAt': serializer.toJson(addedAt), + 'downloadSubtitles': serializer.toJson(downloadSubtitles), + 'downloadArtwork': serializer.toJson(downloadArtwork), + }; + } + + DownloadQueueItem copyWith({ + int? id, + String? mediaGlobalKey, + int? priority, + int? addedAt, + bool? downloadSubtitles, + bool? downloadArtwork, + }) => DownloadQueueItem( + id: id ?? this.id, + mediaGlobalKey: mediaGlobalKey ?? this.mediaGlobalKey, + priority: priority ?? this.priority, + addedAt: addedAt ?? this.addedAt, + downloadSubtitles: downloadSubtitles ?? this.downloadSubtitles, + downloadArtwork: downloadArtwork ?? this.downloadArtwork, + ); + DownloadQueueItem copyWithCompanion(DownloadQueueCompanion data) { + return DownloadQueueItem( + id: data.id.present ? data.id.value : this.id, + mediaGlobalKey: data.mediaGlobalKey.present + ? data.mediaGlobalKey.value + : this.mediaGlobalKey, + priority: data.priority.present ? data.priority.value : this.priority, + addedAt: data.addedAt.present ? data.addedAt.value : this.addedAt, + downloadSubtitles: data.downloadSubtitles.present + ? data.downloadSubtitles.value + : this.downloadSubtitles, + downloadArtwork: data.downloadArtwork.present + ? data.downloadArtwork.value + : this.downloadArtwork, + ); + } + + @override + String toString() { + return (StringBuffer('DownloadQueueItem(') + ..write('id: $id, ') + ..write('mediaGlobalKey: $mediaGlobalKey, ') + ..write('priority: $priority, ') + ..write('addedAt: $addedAt, ') + ..write('downloadSubtitles: $downloadSubtitles, ') + ..write('downloadArtwork: $downloadArtwork') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DownloadQueueItem && + other.id == this.id && + other.mediaGlobalKey == this.mediaGlobalKey && + other.priority == this.priority && + other.addedAt == this.addedAt && + other.downloadSubtitles == this.downloadSubtitles && + other.downloadArtwork == this.downloadArtwork); +} + +class DownloadQueueCompanion extends UpdateCompanion { + final Value id; + final Value mediaGlobalKey; + final Value priority; + final Value addedAt; + final Value downloadSubtitles; + final Value downloadArtwork; + const DownloadQueueCompanion({ + this.id = const Value.absent(), + this.mediaGlobalKey = const Value.absent(), + this.priority = const Value.absent(), + this.addedAt = const Value.absent(), + this.downloadSubtitles = const Value.absent(), + this.downloadArtwork = const Value.absent(), + }); + DownloadQueueCompanion.insert({ + this.id = const Value.absent(), + required String mediaGlobalKey, + this.priority = const Value.absent(), + required int addedAt, + this.downloadSubtitles = const Value.absent(), + this.downloadArtwork = const Value.absent(), + }) : mediaGlobalKey = Value(mediaGlobalKey), + addedAt = Value(addedAt); + static Insertable custom({ + Expression? id, + Expression? mediaGlobalKey, + Expression? priority, + Expression? addedAt, + Expression? downloadSubtitles, + Expression? downloadArtwork, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (mediaGlobalKey != null) 'media_global_key': mediaGlobalKey, + if (priority != null) 'priority': priority, + if (addedAt != null) 'added_at': addedAt, + if (downloadSubtitles != null) 'download_subtitles': downloadSubtitles, + if (downloadArtwork != null) 'download_artwork': downloadArtwork, + }); + } + + DownloadQueueCompanion copyWith({ + Value? id, + Value? mediaGlobalKey, + Value? priority, + Value? addedAt, + Value? downloadSubtitles, + Value? downloadArtwork, + }) { + return DownloadQueueCompanion( + id: id ?? this.id, + mediaGlobalKey: mediaGlobalKey ?? this.mediaGlobalKey, + priority: priority ?? this.priority, + addedAt: addedAt ?? this.addedAt, + downloadSubtitles: downloadSubtitles ?? this.downloadSubtitles, + downloadArtwork: downloadArtwork ?? this.downloadArtwork, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (mediaGlobalKey.present) { + map['media_global_key'] = Variable(mediaGlobalKey.value); + } + if (priority.present) { + map['priority'] = Variable(priority.value); + } + if (addedAt.present) { + map['added_at'] = Variable(addedAt.value); + } + if (downloadSubtitles.present) { + map['download_subtitles'] = Variable(downloadSubtitles.value); + } + if (downloadArtwork.present) { + map['download_artwork'] = Variable(downloadArtwork.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DownloadQueueCompanion(') + ..write('id: $id, ') + ..write('mediaGlobalKey: $mediaGlobalKey, ') + ..write('priority: $priority, ') + ..write('addedAt: $addedAt, ') + ..write('downloadSubtitles: $downloadSubtitles, ') + ..write('downloadArtwork: $downloadArtwork') + ..write(')')) + .toString(); + } +} + +class $ApiCacheTable extends ApiCache + with TableInfo<$ApiCacheTable, ApiCacheData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ApiCacheTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _cacheKeyMeta = const VerificationMeta( + 'cacheKey', + ); + @override + late final GeneratedColumn cacheKey = GeneratedColumn( + 'cache_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pinned" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _cachedAtMeta = const VerificationMeta( + 'cachedAt', + ); + @override + late final GeneratedColumn cachedAt = GeneratedColumn( + 'cached_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [cacheKey, data, pinned, cachedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'api_cache'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('cache_key')) { + context.handle( + _cacheKeyMeta, + cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta), + ); + } else if (isInserting) { + context.missing(_cacheKeyMeta); + } + if (data.containsKey('data')) { + context.handle( + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); + } else if (isInserting) { + context.missing(_dataMeta); + } + if (data.containsKey('pinned')) { + context.handle( + _pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta), + ); + } + if (data.containsKey('cached_at')) { + context.handle( + _cachedAtMeta, + cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {cacheKey}; + @override + ApiCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ApiCacheData( + cacheKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cache_key'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + pinned: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pinned'], + )!, + cachedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}cached_at'], + )!, + ); + } + + @override + $ApiCacheTable createAlias(String alias) { + return $ApiCacheTable(attachedDatabase, alias); + } +} + +class ApiCacheData extends DataClass implements Insertable { + /// Composite key: serverId:endpoint (e.g., "abc123:/library/metadata/12345") + final String cacheKey; + + /// JSON response data + final String data; + + /// Whether this item is pinned for offline access + final bool pinned; + + /// Timestamp for cache invalidation (optional future use) + final DateTime cachedAt; + const ApiCacheData({ + required this.cacheKey, + required this.data, + required this.pinned, + required this.cachedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['cache_key'] = Variable(cacheKey); + map['data'] = Variable(data); + map['pinned'] = Variable(pinned); + map['cached_at'] = Variable(cachedAt); + return map; + } + + ApiCacheCompanion toCompanion(bool nullToAbsent) { + return ApiCacheCompanion( + cacheKey: Value(cacheKey), + data: Value(data), + pinned: Value(pinned), + cachedAt: Value(cachedAt), + ); + } + + factory ApiCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ApiCacheData( + cacheKey: serializer.fromJson(json['cacheKey']), + data: serializer.fromJson(json['data']), + pinned: serializer.fromJson(json['pinned']), + cachedAt: serializer.fromJson(json['cachedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'cacheKey': serializer.toJson(cacheKey), + 'data': serializer.toJson(data), + 'pinned': serializer.toJson(pinned), + 'cachedAt': serializer.toJson(cachedAt), + }; + } + + ApiCacheData copyWith({ + String? cacheKey, + String? data, + bool? pinned, + DateTime? cachedAt, + }) => ApiCacheData( + cacheKey: cacheKey ?? this.cacheKey, + data: data ?? this.data, + pinned: pinned ?? this.pinned, + cachedAt: cachedAt ?? this.cachedAt, + ); + ApiCacheData copyWithCompanion(ApiCacheCompanion data) { + return ApiCacheData( + cacheKey: data.cacheKey.present ? data.cacheKey.value : this.cacheKey, + data: data.data.present ? data.data.value : this.data, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + cachedAt: data.cachedAt.present ? data.cachedAt.value : this.cachedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ApiCacheData(') + ..write('cacheKey: $cacheKey, ') + ..write('data: $data, ') + ..write('pinned: $pinned, ') + ..write('cachedAt: $cachedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(cacheKey, data, pinned, cachedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ApiCacheData && + other.cacheKey == this.cacheKey && + other.data == this.data && + other.pinned == this.pinned && + other.cachedAt == this.cachedAt); +} + +class ApiCacheCompanion extends UpdateCompanion { + final Value cacheKey; + final Value data; + final Value pinned; + final Value cachedAt; + final Value rowid; + const ApiCacheCompanion({ + this.cacheKey = const Value.absent(), + this.data = const Value.absent(), + this.pinned = const Value.absent(), + this.cachedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ApiCacheCompanion.insert({ + required String cacheKey, + required String data, + this.pinned = const Value.absent(), + this.cachedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : cacheKey = Value(cacheKey), + data = Value(data); + static Insertable custom({ + Expression? cacheKey, + Expression? data, + Expression? pinned, + Expression? cachedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (cacheKey != null) 'cache_key': cacheKey, + if (data != null) 'data': data, + if (pinned != null) 'pinned': pinned, + if (cachedAt != null) 'cached_at': cachedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ApiCacheCompanion copyWith({ + Value? cacheKey, + Value? data, + Value? pinned, + Value? cachedAt, + Value? rowid, + }) { + return ApiCacheCompanion( + cacheKey: cacheKey ?? this.cacheKey, + data: data ?? this.data, + pinned: pinned ?? this.pinned, + cachedAt: cachedAt ?? this.cachedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (cacheKey.present) { + map['cache_key'] = Variable(cacheKey.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (cachedAt.present) { + map['cached_at'] = Variable(cachedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ApiCacheCompanion(') + ..write('cacheKey: $cacheKey, ') + ..write('data: $data, ') + ..write('pinned: $pinned, ') + ..write('cachedAt: $cachedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OfflineWatchProgressTable extends OfflineWatchProgress + with TableInfo<$OfflineWatchProgressTable, OfflineWatchProgressItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OfflineWatchProgressTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); + @override + late final GeneratedColumn ratingKey = GeneratedColumn( + 'rating_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); + @override + late final GeneratedColumn globalKey = GeneratedColumn( + 'global_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _actionTypeMeta = const VerificationMeta( + 'actionType', + ); + @override + late final GeneratedColumn actionType = GeneratedColumn( + 'action_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _viewOffsetMeta = const VerificationMeta( + 'viewOffset', + ); + @override + late final GeneratedColumn viewOffset = GeneratedColumn( + 'view_offset', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _durationMeta = const VerificationMeta( + 'duration', + ); + @override + late final GeneratedColumn duration = GeneratedColumn( + 'duration', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta( + 'shouldMarkWatched', + ); + @override + late final GeneratedColumn shouldMarkWatched = GeneratedColumn( + 'should_mark_watched', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("should_mark_watched" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _syncAttemptsMeta = const VerificationMeta( + 'syncAttempts', + ); + @override + late final GeneratedColumn syncAttempts = GeneratedColumn( + 'sync_attempts', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _lastErrorMeta = const VerificationMeta( + 'lastError', + ); + @override + late final GeneratedColumn lastError = GeneratedColumn( + 'last_error', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + serverId, + ratingKey, + globalKey, + actionType, + viewOffset, + duration, + shouldMarkWatched, + createdAt, + updatedAt, + syncAttempts, + lastError, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'offline_watch_progress'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('rating_key')) { + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); + } else if (isInserting) { + context.missing(_ratingKeyMeta); + } + if (data.containsKey('global_key')) { + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); + } else if (isInserting) { + context.missing(_globalKeyMeta); + } + if (data.containsKey('action_type')) { + context.handle( + _actionTypeMeta, + actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), + ); + } else if (isInserting) { + context.missing(_actionTypeMeta); + } + if (data.containsKey('view_offset')) { + context.handle( + _viewOffsetMeta, + viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta), + ); + } + if (data.containsKey('duration')) { + context.handle( + _durationMeta, + duration.isAcceptableOrUnknown(data['duration']!, _durationMeta), + ); + } + if (data.containsKey('should_mark_watched')) { + context.handle( + _shouldMarkWatchedMeta, + shouldMarkWatched.isAcceptableOrUnknown( + data['should_mark_watched']!, + _shouldMarkWatchedMeta, + ), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + if (data.containsKey('sync_attempts')) { + context.handle( + _syncAttemptsMeta, + syncAttempts.isAcceptableOrUnknown( + data['sync_attempts']!, + _syncAttemptsMeta, + ), + ); + } + if (data.containsKey('last_error')) { + context.handle( + _lastErrorMeta, + lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + OfflineWatchProgressItem map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OfflineWatchProgressItem( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + actionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_type'], + )!, + viewOffset: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}view_offset'], + ), + duration: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration'], + ), + shouldMarkWatched: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}should_mark_watched'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + syncAttempts: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sync_attempts'], + )!, + lastError: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error'], + ), + ); + } + + @override + $OfflineWatchProgressTable createAlias(String alias) { + return $OfflineWatchProgressTable(attachedDatabase, alias); + } +} + +class OfflineWatchProgressItem extends DataClass + implements Insertable { + /// Auto-incrementing primary key + final int id; + + /// Server ID this media belongs to + final String serverId; + + /// Rating key of the media item + final String ratingKey; + + /// Global key (serverId:ratingKey) for easy lookup + final String globalKey; + + /// Type of action: 'progress', 'watched', 'unwatched' + final String actionType; + + /// Current playback position in milliseconds (for 'progress' actions) + final int? viewOffset; + + /// Duration of the media in milliseconds (for calculating percentage) + final int? duration; + + /// Whether this item should be marked as watched (for progress sync) + /// Auto-set to true when viewOffset >= 90% of duration + final bool shouldMarkWatched; + + /// Timestamp when this action was recorded (milliseconds since epoch) + final int createdAt; + + /// Timestamp when this action was last updated (for merging progress updates) + final int updatedAt; + + /// Number of sync attempts (for retry logic) + final int syncAttempts; + + /// Last sync error message + final String? lastError; + const OfflineWatchProgressItem({ + required this.id, + required this.serverId, + required this.ratingKey, + required this.globalKey, + required this.actionType, + this.viewOffset, + this.duration, + required this.shouldMarkWatched, + required this.createdAt, + required this.updatedAt, + required this.syncAttempts, + this.lastError, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['server_id'] = Variable(serverId); + map['rating_key'] = Variable(ratingKey); + map['global_key'] = Variable(globalKey); + map['action_type'] = Variable(actionType); + if (!nullToAbsent || viewOffset != null) { + map['view_offset'] = Variable(viewOffset); + } + if (!nullToAbsent || duration != null) { + map['duration'] = Variable(duration); + } + map['should_mark_watched'] = Variable(shouldMarkWatched); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['sync_attempts'] = Variable(syncAttempts); + if (!nullToAbsent || lastError != null) { + map['last_error'] = Variable(lastError); + } + return map; + } + + OfflineWatchProgressCompanion toCompanion(bool nullToAbsent) { + return OfflineWatchProgressCompanion( + id: Value(id), + serverId: Value(serverId), + ratingKey: Value(ratingKey), + globalKey: Value(globalKey), + actionType: Value(actionType), + viewOffset: viewOffset == null && nullToAbsent + ? const Value.absent() + : Value(viewOffset), + duration: duration == null && nullToAbsent + ? const Value.absent() + : Value(duration), + shouldMarkWatched: Value(shouldMarkWatched), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + syncAttempts: Value(syncAttempts), + lastError: lastError == null && nullToAbsent + ? const Value.absent() + : Value(lastError), + ); + } + + factory OfflineWatchProgressItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OfflineWatchProgressItem( + id: serializer.fromJson(json['id']), + serverId: serializer.fromJson(json['serverId']), + ratingKey: serializer.fromJson(json['ratingKey']), + globalKey: serializer.fromJson(json['globalKey']), + actionType: serializer.fromJson(json['actionType']), + viewOffset: serializer.fromJson(json['viewOffset']), + duration: serializer.fromJson(json['duration']), + shouldMarkWatched: serializer.fromJson(json['shouldMarkWatched']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + syncAttempts: serializer.fromJson(json['syncAttempts']), + lastError: serializer.fromJson(json['lastError']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'serverId': serializer.toJson(serverId), + 'ratingKey': serializer.toJson(ratingKey), + 'globalKey': serializer.toJson(globalKey), + 'actionType': serializer.toJson(actionType), + 'viewOffset': serializer.toJson(viewOffset), + 'duration': serializer.toJson(duration), + 'shouldMarkWatched': serializer.toJson(shouldMarkWatched), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'syncAttempts': serializer.toJson(syncAttempts), + 'lastError': serializer.toJson(lastError), + }; + } + + OfflineWatchProgressItem copyWith({ + int? id, + String? serverId, + String? ratingKey, + String? globalKey, + String? actionType, + Value viewOffset = const Value.absent(), + Value duration = const Value.absent(), + bool? shouldMarkWatched, + int? createdAt, + int? updatedAt, + int? syncAttempts, + Value lastError = const Value.absent(), + }) => OfflineWatchProgressItem( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + ratingKey: ratingKey ?? this.ratingKey, + globalKey: globalKey ?? this.globalKey, + actionType: actionType ?? this.actionType, + viewOffset: viewOffset.present ? viewOffset.value : this.viewOffset, + duration: duration.present ? duration.value : this.duration, + shouldMarkWatched: shouldMarkWatched ?? this.shouldMarkWatched, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + syncAttempts: syncAttempts ?? this.syncAttempts, + lastError: lastError.present ? lastError.value : this.lastError, + ); + OfflineWatchProgressItem copyWithCompanion( + OfflineWatchProgressCompanion data, + ) { + return OfflineWatchProgressItem( + id: data.id.present ? data.id.value : this.id, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + ratingKey: data.ratingKey.present ? data.ratingKey.value : this.ratingKey, + globalKey: data.globalKey.present ? data.globalKey.value : this.globalKey, + actionType: data.actionType.present + ? data.actionType.value + : this.actionType, + viewOffset: data.viewOffset.present + ? data.viewOffset.value + : this.viewOffset, + duration: data.duration.present ? data.duration.value : this.duration, + shouldMarkWatched: data.shouldMarkWatched.present + ? data.shouldMarkWatched.value + : this.shouldMarkWatched, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + syncAttempts: data.syncAttempts.present + ? data.syncAttempts.value + : this.syncAttempts, + lastError: data.lastError.present ? data.lastError.value : this.lastError, + ); + } + + @override + String toString() { + return (StringBuffer('OfflineWatchProgressItem(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('ratingKey: $ratingKey, ') + ..write('globalKey: $globalKey, ') + ..write('actionType: $actionType, ') + ..write('viewOffset: $viewOffset, ') + ..write('duration: $duration, ') + ..write('shouldMarkWatched: $shouldMarkWatched, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('syncAttempts: $syncAttempts, ') + ..write('lastError: $lastError') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + serverId, + ratingKey, + globalKey, + actionType, + viewOffset, + duration, + shouldMarkWatched, + createdAt, + updatedAt, + syncAttempts, + lastError, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OfflineWatchProgressItem && + other.id == this.id && + other.serverId == this.serverId && + other.ratingKey == this.ratingKey && + other.globalKey == this.globalKey && + other.actionType == this.actionType && + other.viewOffset == this.viewOffset && + other.duration == this.duration && + other.shouldMarkWatched == this.shouldMarkWatched && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.syncAttempts == this.syncAttempts && + other.lastError == this.lastError); +} + +class OfflineWatchProgressCompanion + extends UpdateCompanion { + final Value id; + final Value serverId; + final Value ratingKey; + final Value globalKey; + final Value actionType; + final Value viewOffset; + final Value duration; + final Value shouldMarkWatched; + final Value createdAt; + final Value updatedAt; + final Value syncAttempts; + final Value lastError; + const OfflineWatchProgressCompanion({ + this.id = const Value.absent(), + this.serverId = const Value.absent(), + this.ratingKey = const Value.absent(), + this.globalKey = const Value.absent(), + this.actionType = const Value.absent(), + this.viewOffset = const Value.absent(), + this.duration = const Value.absent(), + this.shouldMarkWatched = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.syncAttempts = const Value.absent(), + this.lastError = const Value.absent(), + }); + OfflineWatchProgressCompanion.insert({ + this.id = const Value.absent(), + required String serverId, + required String ratingKey, + required String globalKey, + required String actionType, + this.viewOffset = const Value.absent(), + this.duration = const Value.absent(), + this.shouldMarkWatched = const Value.absent(), + required int createdAt, + required int updatedAt, + this.syncAttempts = const Value.absent(), + this.lastError = const Value.absent(), + }) : serverId = Value(serverId), + ratingKey = Value(ratingKey), + globalKey = Value(globalKey), + actionType = Value(actionType), + createdAt = Value(createdAt), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? serverId, + Expression? ratingKey, + Expression? globalKey, + Expression? actionType, + Expression? viewOffset, + Expression? duration, + Expression? shouldMarkWatched, + Expression? createdAt, + Expression? updatedAt, + Expression? syncAttempts, + Expression? lastError, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (serverId != null) 'server_id': serverId, + if (ratingKey != null) 'rating_key': ratingKey, + if (globalKey != null) 'global_key': globalKey, + if (actionType != null) 'action_type': actionType, + if (viewOffset != null) 'view_offset': viewOffset, + if (duration != null) 'duration': duration, + if (shouldMarkWatched != null) 'should_mark_watched': shouldMarkWatched, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (syncAttempts != null) 'sync_attempts': syncAttempts, + if (lastError != null) 'last_error': lastError, + }); + } + + OfflineWatchProgressCompanion copyWith({ + Value? id, + Value? serverId, + Value? ratingKey, + Value? globalKey, + Value? actionType, + Value? viewOffset, + Value? duration, + Value? shouldMarkWatched, + Value? createdAt, + Value? updatedAt, + Value? syncAttempts, + Value? lastError, + }) { + return OfflineWatchProgressCompanion( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + ratingKey: ratingKey ?? this.ratingKey, + globalKey: globalKey ?? this.globalKey, + actionType: actionType ?? this.actionType, + viewOffset: viewOffset ?? this.viewOffset, + duration: duration ?? this.duration, + shouldMarkWatched: shouldMarkWatched ?? this.shouldMarkWatched, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + syncAttempts: syncAttempts ?? this.syncAttempts, + lastError: lastError ?? this.lastError, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (ratingKey.present) { + map['rating_key'] = Variable(ratingKey.value); + } + if (globalKey.present) { + map['global_key'] = Variable(globalKey.value); + } + if (actionType.present) { + map['action_type'] = Variable(actionType.value); + } + if (viewOffset.present) { + map['view_offset'] = Variable(viewOffset.value); + } + if (duration.present) { + map['duration'] = Variable(duration.value); + } + if (shouldMarkWatched.present) { + map['should_mark_watched'] = Variable(shouldMarkWatched.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (syncAttempts.present) { + map['sync_attempts'] = Variable(syncAttempts.value); + } + if (lastError.present) { + map['last_error'] = Variable(lastError.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OfflineWatchProgressCompanion(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('ratingKey: $ratingKey, ') + ..write('globalKey: $globalKey, ') + ..write('actionType: $actionType, ') + ..write('viewOffset: $viewOffset, ') + ..write('duration: $duration, ') + ..write('shouldMarkWatched: $shouldMarkWatched, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('syncAttempts: $syncAttempts, ') + ..write('lastError: $lastError') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable( + this, + ); + late final $DownloadQueueTable downloadQueue = $DownloadQueueTable(this); + late final $ApiCacheTable apiCache = $ApiCacheTable(this); + late final $OfflineWatchProgressTable offlineWatchProgress = + $OfflineWatchProgressTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + downloadedMedia, + downloadQueue, + apiCache, + offlineWatchProgress, + ]; +} + +typedef $$DownloadedMediaTableCreateCompanionBuilder = + DownloadedMediaCompanion Function({ + Value id, + required String serverId, + required String ratingKey, + required String globalKey, + required String type, + Value parentRatingKey, + Value grandparentRatingKey, + required int status, + Value progress, + Value totalBytes, + Value downloadedBytes, + Value videoFilePath, + Value thumbPath, + Value downloadedAt, + Value errorMessage, + Value retryCount, + }); +typedef $$DownloadedMediaTableUpdateCompanionBuilder = + DownloadedMediaCompanion Function({ + Value id, + Value serverId, + Value ratingKey, + Value globalKey, + Value type, + Value parentRatingKey, + Value grandparentRatingKey, + Value status, + Value progress, + Value totalBytes, + Value downloadedBytes, + Value videoFilePath, + Value thumbPath, + Value downloadedAt, + Value errorMessage, + Value retryCount, + }); + +class $$DownloadedMediaTableFilterComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { + $$DownloadedMediaTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnFilters(column), + ); +} + +class $$DownloadedMediaTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { + $$DownloadedMediaTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$DownloadedMediaTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { + $$DownloadedMediaTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); + + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => column, + ); + + GeneratedColumn get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => column, + ); + + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); + + GeneratedColumn get progress => + $composableBuilder(column: $table.progress, builder: (column) => column); + + GeneratedColumn get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => column, + ); + + GeneratedColumn get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => column, + ); + + GeneratedColumn get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => column, + ); + + GeneratedColumn get thumbPath => + $composableBuilder(column: $table.thumbPath, builder: (column) => column); + + GeneratedColumn get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => column, + ); + + GeneratedColumn get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => column, + ); + + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => column, + ); +} + +class $$DownloadedMediaTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem, + $$DownloadedMediaTableFilterComposer, + $$DownloadedMediaTableOrderingComposer, + $$DownloadedMediaTableAnnotationComposer, + $$DownloadedMediaTableCreateCompanionBuilder, + $$DownloadedMediaTableUpdateCompanionBuilder, + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), + DownloadedMediaItem, + PrefetchHooks Function() + > { + $$DownloadedMediaTableTableManager( + _$AppDatabase db, + $DownloadedMediaTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DownloadedMediaTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadedMediaTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value serverId = const Value.absent(), + Value ratingKey = const Value.absent(), + Value globalKey = const Value.absent(), + Value type = const Value.absent(), + Value parentRatingKey = const Value.absent(), + Value grandparentRatingKey = const Value.absent(), + Value status = const Value.absent(), + Value progress = const Value.absent(), + Value totalBytes = const Value.absent(), + Value downloadedBytes = const Value.absent(), + Value videoFilePath = const Value.absent(), + Value thumbPath = const Value.absent(), + Value downloadedAt = const Value.absent(), + Value errorMessage = const Value.absent(), + Value retryCount = const Value.absent(), + }) => DownloadedMediaCompanion( + id: id, + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + type: type, + parentRatingKey: parentRatingKey, + grandparentRatingKey: grandparentRatingKey, + status: status, + progress: progress, + totalBytes: totalBytes, + downloadedBytes: downloadedBytes, + videoFilePath: videoFilePath, + thumbPath: thumbPath, + downloadedAt: downloadedAt, + errorMessage: errorMessage, + retryCount: retryCount, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String serverId, + required String ratingKey, + required String globalKey, + required String type, + Value parentRatingKey = const Value.absent(), + Value grandparentRatingKey = const Value.absent(), + required int status, + Value progress = const Value.absent(), + Value totalBytes = const Value.absent(), + Value downloadedBytes = const Value.absent(), + Value videoFilePath = const Value.absent(), + Value thumbPath = const Value.absent(), + Value downloadedAt = const Value.absent(), + Value errorMessage = const Value.absent(), + Value retryCount = const Value.absent(), + }) => DownloadedMediaCompanion.insert( + id: id, + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + type: type, + parentRatingKey: parentRatingKey, + grandparentRatingKey: grandparentRatingKey, + status: status, + progress: progress, + totalBytes: totalBytes, + downloadedBytes: downloadedBytes, + videoFilePath: videoFilePath, + thumbPath: thumbPath, + downloadedAt: downloadedAt, + errorMessage: errorMessage, + retryCount: retryCount, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$DownloadedMediaTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem, + $$DownloadedMediaTableFilterComposer, + $$DownloadedMediaTableOrderingComposer, + $$DownloadedMediaTableAnnotationComposer, + $$DownloadedMediaTableCreateCompanionBuilder, + $$DownloadedMediaTableUpdateCompanionBuilder, + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), + DownloadedMediaItem, + PrefetchHooks Function() + >; +typedef $$DownloadQueueTableCreateCompanionBuilder = + DownloadQueueCompanion Function({ + Value id, + required String mediaGlobalKey, + Value priority, + required int addedAt, + Value downloadSubtitles, + Value downloadArtwork, + }); +typedef $$DownloadQueueTableUpdateCompanionBuilder = + DownloadQueueCompanion Function({ + Value id, + Value mediaGlobalKey, + Value priority, + Value addedAt, + Value downloadSubtitles, + Value downloadArtwork, + }); + +class $$DownloadQueueTableFilterComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { + $$DownloadQueueTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnFilters(column), + ); +} + +class $$DownloadQueueTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { + $$DownloadQueueTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$DownloadQueueTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { + $$DownloadQueueTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => column, + ); + + GeneratedColumn get priority => + $composableBuilder(column: $table.priority, builder: (column) => column); + + GeneratedColumn get addedAt => + $composableBuilder(column: $table.addedAt, builder: (column) => column); + + GeneratedColumn get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => column, + ); + + GeneratedColumn get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => column, + ); +} + +class $$DownloadQueueTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DownloadQueueTable, + DownloadQueueItem, + $$DownloadQueueTableFilterComposer, + $$DownloadQueueTableOrderingComposer, + $$DownloadQueueTableAnnotationComposer, + $$DownloadQueueTableCreateCompanionBuilder, + $$DownloadQueueTableUpdateCompanionBuilder, + ( + DownloadQueueItem, + BaseReferences< + _$AppDatabase, + $DownloadQueueTable, + DownloadQueueItem + >, + ), + DownloadQueueItem, + PrefetchHooks Function() + > { + $$DownloadQueueTableTableManager(_$AppDatabase db, $DownloadQueueTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DownloadQueueTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadQueueTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadQueueTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value mediaGlobalKey = const Value.absent(), + Value priority = const Value.absent(), + Value addedAt = const Value.absent(), + Value downloadSubtitles = const Value.absent(), + Value downloadArtwork = const Value.absent(), + }) => DownloadQueueCompanion( + id: id, + mediaGlobalKey: mediaGlobalKey, + priority: priority, + addedAt: addedAt, + downloadSubtitles: downloadSubtitles, + downloadArtwork: downloadArtwork, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String mediaGlobalKey, + Value priority = const Value.absent(), + required int addedAt, + Value downloadSubtitles = const Value.absent(), + Value downloadArtwork = const Value.absent(), + }) => DownloadQueueCompanion.insert( + id: id, + mediaGlobalKey: mediaGlobalKey, + priority: priority, + addedAt: addedAt, + downloadSubtitles: downloadSubtitles, + downloadArtwork: downloadArtwork, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$DownloadQueueTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DownloadQueueTable, + DownloadQueueItem, + $$DownloadQueueTableFilterComposer, + $$DownloadQueueTableOrderingComposer, + $$DownloadQueueTableAnnotationComposer, + $$DownloadQueueTableCreateCompanionBuilder, + $$DownloadQueueTableUpdateCompanionBuilder, + ( + DownloadQueueItem, + BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>, + ), + DownloadQueueItem, + PrefetchHooks Function() + >; +typedef $$ApiCacheTableCreateCompanionBuilder = + ApiCacheCompanion Function({ + required String cacheKey, + required String data, + Value pinned, + Value cachedAt, + Value rowid, + }); +typedef $$ApiCacheTableUpdateCompanionBuilder = + ApiCacheCompanion Function({ + Value cacheKey, + Value data, + Value pinned, + Value cachedAt, + Value rowid, + }); + +class $$ApiCacheTableFilterComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { + $$ApiCacheTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ApiCacheTableOrderingComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { + $$ApiCacheTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ApiCacheTableAnnotationComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { + $$ApiCacheTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get cacheKey => + $composableBuilder(column: $table.cacheKey, builder: (column) => column); + + GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); + + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); + + GeneratedColumn get cachedAt => + $composableBuilder(column: $table.cachedAt, builder: (column) => column); +} + +class $$ApiCacheTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ApiCacheTable, + ApiCacheData, + $$ApiCacheTableFilterComposer, + $$ApiCacheTableOrderingComposer, + $$ApiCacheTableAnnotationComposer, + $$ApiCacheTableCreateCompanionBuilder, + $$ApiCacheTableUpdateCompanionBuilder, + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), + ApiCacheData, + PrefetchHooks Function() + > { + $$ApiCacheTableTableManager(_$AppDatabase db, $ApiCacheTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ApiCacheTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ApiCacheTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ApiCacheTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value cacheKey = const Value.absent(), + Value data = const Value.absent(), + Value pinned = const Value.absent(), + Value cachedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ApiCacheCompanion( + cacheKey: cacheKey, + data: data, + pinned: pinned, + cachedAt: cachedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String cacheKey, + required String data, + Value pinned = const Value.absent(), + Value cachedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ApiCacheCompanion.insert( + cacheKey: cacheKey, + data: data, + pinned: pinned, + cachedAt: cachedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ApiCacheTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ApiCacheTable, + ApiCacheData, + $$ApiCacheTableFilterComposer, + $$ApiCacheTableOrderingComposer, + $$ApiCacheTableAnnotationComposer, + $$ApiCacheTableCreateCompanionBuilder, + $$ApiCacheTableUpdateCompanionBuilder, + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), + ApiCacheData, + PrefetchHooks Function() + >; +typedef $$OfflineWatchProgressTableCreateCompanionBuilder = + OfflineWatchProgressCompanion Function({ + Value id, + required String serverId, + required String ratingKey, + required String globalKey, + required String actionType, + Value viewOffset, + Value duration, + Value shouldMarkWatched, + required int createdAt, + required int updatedAt, + Value syncAttempts, + Value lastError, + }); +typedef $$OfflineWatchProgressTableUpdateCompanionBuilder = + OfflineWatchProgressCompanion Function({ + Value id, + Value serverId, + Value ratingKey, + Value globalKey, + Value actionType, + Value viewOffset, + Value duration, + Value shouldMarkWatched, + Value createdAt, + Value updatedAt, + Value syncAttempts, + Value lastError, + }); + +class $$OfflineWatchProgressTableFilterComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { + $$OfflineWatchProgressTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnFilters(column), + ); +} + +class $$OfflineWatchProgressTableOrderingComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { + $$OfflineWatchProgressTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$OfflineWatchProgressTableAnnotationComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { + $$OfflineWatchProgressTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); + + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); + + GeneratedColumn get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => column, + ); + + GeneratedColumn get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => column, + ); + + GeneratedColumn get duration => + $composableBuilder(column: $table.duration, builder: (column) => column); + + GeneratedColumn get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => column, + ); + + GeneratedColumn get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => column); +} + +class $$OfflineWatchProgressTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem, + $$OfflineWatchProgressTableFilterComposer, + $$OfflineWatchProgressTableOrderingComposer, + $$OfflineWatchProgressTableAnnotationComposer, + $$OfflineWatchProgressTableCreateCompanionBuilder, + $$OfflineWatchProgressTableUpdateCompanionBuilder, + ( + OfflineWatchProgressItem, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, + ), + OfflineWatchProgressItem, + PrefetchHooks Function() + > { + $$OfflineWatchProgressTableTableManager( + _$AppDatabase db, + $OfflineWatchProgressTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OfflineWatchProgressTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$OfflineWatchProgressTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value serverId = const Value.absent(), + Value ratingKey = const Value.absent(), + Value globalKey = const Value.absent(), + Value actionType = const Value.absent(), + Value viewOffset = const Value.absent(), + Value duration = const Value.absent(), + Value shouldMarkWatched = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value syncAttempts = const Value.absent(), + Value lastError = const Value.absent(), + }) => OfflineWatchProgressCompanion( + id: id, + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + actionType: actionType, + viewOffset: viewOffset, + duration: duration, + shouldMarkWatched: shouldMarkWatched, + createdAt: createdAt, + updatedAt: updatedAt, + syncAttempts: syncAttempts, + lastError: lastError, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String serverId, + required String ratingKey, + required String globalKey, + required String actionType, + Value viewOffset = const Value.absent(), + Value duration = const Value.absent(), + Value shouldMarkWatched = const Value.absent(), + required int createdAt, + required int updatedAt, + Value syncAttempts = const Value.absent(), + Value lastError = const Value.absent(), + }) => OfflineWatchProgressCompanion.insert( + id: id, + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + actionType: actionType, + viewOffset: viewOffset, + duration: duration, + shouldMarkWatched: shouldMarkWatched, + createdAt: createdAt, + updatedAt: updatedAt, + syncAttempts: syncAttempts, + lastError: lastError, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$OfflineWatchProgressTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem, + $$OfflineWatchProgressTableFilterComposer, + $$OfflineWatchProgressTableOrderingComposer, + $$OfflineWatchProgressTableAnnotationComposer, + $$OfflineWatchProgressTableCreateCompanionBuilder, + $$OfflineWatchProgressTableUpdateCompanionBuilder, + ( + OfflineWatchProgressItem, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, + ), + OfflineWatchProgressItem, + PrefetchHooks Function() + >; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$DownloadedMediaTableTableManager get downloadedMedia => + $$DownloadedMediaTableTableManager(_db, _db.downloadedMedia); + $$DownloadQueueTableTableManager get downloadQueue => + $$DownloadQueueTableTableManager(_db, _db.downloadQueue); + $$ApiCacheTableTableManager get apiCache => + $$ApiCacheTableTableManager(_db, _db.apiCache); + $$OfflineWatchProgressTableTableManager get offlineWatchProgress => + $$OfflineWatchProgressTableTableManager(_db, _db.offlineWatchProgress); +} diff --git a/lib/database/tables/api_cache.dart b/lib/database/tables/api_cache.dart new file mode 100644 index 00000000..dcafcb5c --- /dev/null +++ b/lib/database/tables/api_cache.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; + +/// Key-value cache table for Plex API responses. +/// Used for offline support - stores raw JSON responses. +class ApiCache extends Table { + /// Composite key: serverId:endpoint (e.g., "abc123:/library/metadata/12345") + TextColumn get cacheKey => text()(); + + /// JSON response data + TextColumn get data => text()(); + + /// Whether this item is pinned for offline access + BoolColumn get pinned => boolean().withDefault(const Constant(false))(); + + /// Timestamp for cache invalidation (optional future use) + DateTimeColumn get cachedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {cacheKey}; +} diff --git a/lib/database/tables/download_queue.dart b/lib/database/tables/download_queue.dart new file mode 100644 index 00000000..f4bee463 --- /dev/null +++ b/lib/database/tables/download_queue.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; + +@DataClassName('DownloadQueueItem') +class DownloadQueue extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get mediaGlobalKey => text().unique()(); + IntColumn get priority => integer().withDefault(const Constant(0))(); + IntColumn get addedAt => integer()(); + BoolColumn get downloadSubtitles => + boolean().withDefault(const Constant(true))(); + BoolColumn get downloadArtwork => + boolean().withDefault(const Constant(true))(); +} diff --git a/lib/database/tables/downloaded_media.dart b/lib/database/tables/downloaded_media.dart new file mode 100644 index 00000000..582d802a --- /dev/null +++ b/lib/database/tables/downloaded_media.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; + +@DataClassName('DownloadedMediaItem') +class DownloadedMedia extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get serverId => text()(); + TextColumn get ratingKey => text()(); + TextColumn get globalKey => text().unique()(); + TextColumn get type => text()(); + TextColumn get parentRatingKey => text().nullable()(); + TextColumn get grandparentRatingKey => text().nullable()(); + IntColumn get status => integer()(); + IntColumn get progress => integer().withDefault(const Constant(0))(); + IntColumn get totalBytes => integer().nullable()(); + IntColumn get downloadedBytes => integer().withDefault(const Constant(0))(); + TextColumn get videoFilePath => text().nullable()(); + TextColumn get thumbPath => text().nullable()(); + IntColumn get downloadedAt => integer().nullable()(); + TextColumn get errorMessage => text().nullable()(); + IntColumn get retryCount => integer().withDefault(const Constant(0))(); +} diff --git a/lib/database/tables/offline_watch_progress.dart b/lib/database/tables/offline_watch_progress.dart new file mode 100644 index 00000000..c6bf11db --- /dev/null +++ b/lib/database/tables/offline_watch_progress.dart @@ -0,0 +1,46 @@ +import 'package:drift/drift.dart'; + +/// Queue for offline watch progress and manual watch actions. +/// +/// Stores watch progress updates and manual watch/unwatch actions +/// that need to be synced to the Plex server when back online. +@DataClassName('OfflineWatchProgressItem') +class OfflineWatchProgress extends Table { + /// Auto-incrementing primary key + IntColumn get id => integer().autoIncrement()(); + + /// Server ID this media belongs to + TextColumn get serverId => text()(); + + /// Rating key of the media item + TextColumn get ratingKey => text()(); + + /// Global key (serverId:ratingKey) for easy lookup + TextColumn get globalKey => text()(); + + /// Type of action: 'progress', 'watched', 'unwatched' + TextColumn get actionType => text()(); + + /// Current playback position in milliseconds (for 'progress' actions) + IntColumn get viewOffset => integer().nullable()(); + + /// Duration of the media in milliseconds (for calculating percentage) + IntColumn get duration => integer().nullable()(); + + /// Whether this item should be marked as watched (for progress sync) + /// Auto-set to true when viewOffset >= 90% of duration + BoolColumn get shouldMarkWatched => + boolean().withDefault(const Constant(false))(); + + /// Timestamp when this action was recorded (milliseconds since epoch) + IntColumn get createdAt => integer()(); + + /// Timestamp when this action was last updated (for merging progress updates) + IntColumn get updatedAt => integer()(); + + /// Number of sync attempts (for retry logic) + IntColumn get syncAttempts => integer().withDefault(const Constant(0))(); + + /// Last sync error message + TextColumn get lastError => text().nullable()(); +} diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 8871022e..4f6f4e52 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 2710 (451 per locale) +/// Strings: 3076 (512 per locale) /// -/// Built on 2025-12-07 at 18:34 UTC +/// Built on 2025-12-11 at 18:32 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -25,22 +25,32 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build), - de(languageCode: 'de', build: _StringsDe.build), - it(languageCode: 'it', build: _StringsIt.build), - nl(languageCode: 'nl', build: _StringsNl.build), - sv(languageCode: 'sv', build: _StringsSv.build), - zh(languageCode: 'zh', build: _StringsZh.build); + en(languageCode: 'en', build: Translations.build), + de(languageCode: 'de', build: _StringsDe.build), + it(languageCode: 'it', build: _StringsIt.build), + nl(languageCode: 'nl', build: _StringsNl.build), + sv(languageCode: 'sv', build: _StringsSv.build), + zh(languageCode: 'zh', build: _StringsZh.build); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale({ + required this.languageCode, + this.scriptCode, + this.countryCode, + required this.build, + }); // ignore: unused_element - @override final String languageCode; - @override final String? scriptCode; - @override final String? countryCode; - @override final TranslationBuilder build; + @override + final String languageCode; + @override + final String? scriptCode; + @override + final String? countryCode; + @override + final TranslationBuilder build; - /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => + LocaleSettings.instance.translationMap[this]!; } /// Method A: Simple @@ -69,10 +79,14 @@ Translations get t => LocaleSettings.instance.currentTranslations; /// final t = Translations.of(context); // Get t variable. /// String a = t.someKey.anotherKey; // Use t variable. /// String b = t['someKey.anotherKey']; // Only for edge cases! -class TranslationProvider extends BaseTranslationProvider { - TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); +class TranslationProvider + extends BaseTranslationProvider { + TranslationProvider({required super.child}) + : super(settings: LocaleSettings.instance); - static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); + static InheritedLocaleData of( + BuildContext context, + ) => InheritedLocaleData.of(context); } /// Method B shorthand via [BuildContext] extension method. @@ -81,7596 +95,15425 @@ class TranslationProvider extends BaseTranslationProvider TranslationProvider.of(this).translations; + Translations get t => TranslationProvider.of(this).translations; } /// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); +class LocaleSettings + extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); - static final instance = LocaleSettings._(); + static final instance = LocaleSettings._(); - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static AppLocale setLocale( + AppLocale locale, { + bool? listenToDeviceLocale = false, + }) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRaw( + String rawLocale, { + bool? listenToDeviceLocale = false, + }) => instance.setLocaleRaw( + rawLocale, + listenToDeviceLocale: listenToDeviceLocale, + ); + static AppLocale useDeviceLocale() => instance.useDeviceLocale(); + @Deprecated('Use [AppLocaleUtils.supportedLocales]') + static List get supportedLocales => instance.supportedLocales; + @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') + static List get supportedLocalesRaw => instance.supportedLocalesRaw; + static void setPluralResolver({ + String? language, + AppLocale? locale, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() + : super(baseLocale: _baseLocale, locales: AppLocale.values); - static final instance = AppLocaleUtils._(); + static final instance = AppLocaleUtils._(); - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); - static AppLocale findDeviceLocale() => instance.findDeviceLocale(); - static List get supportedLocales => instance.supportedLocales; - static List get supportedLocalesRaw => instance.supportedLocalesRaw; + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({ + required String languageCode, + String? scriptCode, + String? countryCode, + }) => instance.parseLocaleParts( + languageCode: languageCode, + scriptCode: scriptCode, + countryCode: countryCode, + ); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; } // translations // Path: class Translations implements BaseTranslations { - /// Returns the current translations of the given [context]. - /// - /// Usage: - /// final t = Translations.of(context); - static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => + InheritedLocaleData.of(context).translations; - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + dynamic operator [](String key) => $meta.getTranslation(key); - late final Translations _root = this; // ignore: unused_field + late final Translations _root = this; // ignore: unused_field - // Translations - late final _StringsAppEn app = _StringsAppEn._(_root); - late final _StringsAuthEn auth = _StringsAuthEn._(_root); - late final _StringsCommonEn common = _StringsCommonEn._(_root); - late final _StringsScreensEn screens = _StringsScreensEn._(_root); - late final _StringsUpdateEn update = _StringsUpdateEn._(_root); - late final _StringsSettingsEn settings = _StringsSettingsEn._(_root); - late final _StringsSearchEn search = _StringsSearchEn._(_root); - late final _StringsHotkeysEn hotkeys = _StringsHotkeysEn._(_root); - late final _StringsPinEntryEn pinEntry = _StringsPinEntryEn._(_root); - late final _StringsFileInfoEn fileInfo = _StringsFileInfoEn._(_root); - late final _StringsMediaMenuEn mediaMenu = _StringsMediaMenuEn._(_root); - late final _StringsAccessibilityEn accessibility = _StringsAccessibilityEn._(_root); - late final _StringsTooltipsEn tooltips = _StringsTooltipsEn._(_root); - late final _StringsVideoControlsEn videoControls = _StringsVideoControlsEn._(_root); - late final _StringsUserStatusEn userStatus = _StringsUserStatusEn._(_root); - late final _StringsMessagesEn messages = _StringsMessagesEn._(_root); - late final _StringsProfileEn profile = _StringsProfileEn._(_root); - late final _StringsSubtitlingStylingEn subtitlingStyling = _StringsSubtitlingStylingEn._(_root); - late final _StringsDialogEn dialog = _StringsDialogEn._(_root); - late final _StringsDiscoverEn discover = _StringsDiscoverEn._(_root); - late final _StringsErrorsEn errors = _StringsErrorsEn._(_root); - late final _StringsLibrariesEn libraries = _StringsLibrariesEn._(_root); - late final _StringsAboutEn about = _StringsAboutEn._(_root); - late final _StringsServerSelectionEn serverSelection = _StringsServerSelectionEn._(_root); - late final _StringsHubDetailEn hubDetail = _StringsHubDetailEn._(_root); - late final _StringsLogsEn logs = _StringsLogsEn._(_root); - late final _StringsLicensesEn licenses = _StringsLicensesEn._(_root); - late final _StringsNavigationEn navigation = _StringsNavigationEn._(_root); - late final _StringsCollectionsEn collections = _StringsCollectionsEn._(_root); - late final _StringsPlaylistsEn playlists = _StringsPlaylistsEn._(_root); + // Translations + late final _StringsAppEn app = _StringsAppEn._(_root); + late final _StringsAuthEn auth = _StringsAuthEn._(_root); + late final _StringsCommonEn common = _StringsCommonEn._(_root); + late final _StringsScreensEn screens = _StringsScreensEn._(_root); + late final _StringsUpdateEn update = _StringsUpdateEn._(_root); + late final _StringsSettingsEn settings = _StringsSettingsEn._(_root); + late final _StringsSearchEn search = _StringsSearchEn._(_root); + late final _StringsHotkeysEn hotkeys = _StringsHotkeysEn._(_root); + late final _StringsPinEntryEn pinEntry = _StringsPinEntryEn._(_root); + late final _StringsFileInfoEn fileInfo = _StringsFileInfoEn._(_root); + late final _StringsMediaMenuEn mediaMenu = _StringsMediaMenuEn._(_root); + late final _StringsAccessibilityEn accessibility = _StringsAccessibilityEn._( + _root, + ); + late final _StringsTooltipsEn tooltips = _StringsTooltipsEn._(_root); + late final _StringsVideoControlsEn videoControls = _StringsVideoControlsEn._( + _root, + ); + late final _StringsUserStatusEn userStatus = _StringsUserStatusEn._(_root); + late final _StringsMessagesEn messages = _StringsMessagesEn._(_root); + late final _StringsProfileEn profile = _StringsProfileEn._(_root); + late final _StringsSubtitlingStylingEn subtitlingStyling = + _StringsSubtitlingStylingEn._(_root); + late final _StringsDialogEn dialog = _StringsDialogEn._(_root); + late final _StringsDiscoverEn discover = _StringsDiscoverEn._(_root); + late final _StringsErrorsEn errors = _StringsErrorsEn._(_root); + late final _StringsLibrariesEn libraries = _StringsLibrariesEn._(_root); + late final _StringsAboutEn about = _StringsAboutEn._(_root); + late final _StringsServerSelectionEn serverSelection = + _StringsServerSelectionEn._(_root); + late final _StringsHubDetailEn hubDetail = _StringsHubDetailEn._(_root); + late final _StringsLogsEn logs = _StringsLogsEn._(_root); + late final _StringsLicensesEn licenses = _StringsLicensesEn._(_root); + late final _StringsNavigationEn navigation = _StringsNavigationEn._(_root); + late final _StringsCollectionsEn collections = _StringsCollectionsEn._(_root); + late final _StringsPlaylistsEn playlists = _StringsPlaylistsEn._(_root); + late final _StringsDownloadsEn downloads = _StringsDownloadsEn._(_root); + late final _StringsOfflineEn offline = _StringsOfflineEn._(_root); } // Path: app class _StringsAppEn { - _StringsAppEn._(this._root); + _StringsAppEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Plezy'; - String get loading => 'Loading...'; + // Translations + String get title => 'Plezy'; + String get loading => 'Loading...'; } // Path: auth class _StringsAuthEn { - _StringsAuthEn._(this._root); + _StringsAuthEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get signInWithPlex => 'Sign in with Plex'; - String get showQRCode => 'Show QR Code'; - String get cancel => 'Cancel'; - String get authenticate => 'Authenticate'; - String get retry => 'Retry'; - String get debugEnterToken => 'Debug: Enter Plex Token'; - String get plexTokenLabel => 'Plex Auth Token'; - String get plexTokenHint => 'Enter your Plex.tv token'; - String get authenticationTimeout => 'Authentication timed out. Please try again.'; - String get scanQRCodeInstruction => 'Scan this QR code with a device logged into Plex to authenticate.'; - String get waitingForAuth => 'Waiting for authentication...\nPlease complete sign-in in your browser.'; + // Translations + String get signInWithPlex => 'Sign in with Plex'; + String get showQRCode => 'Show QR Code'; + String get cancel => 'Cancel'; + String get authenticate => 'Authenticate'; + String get retry => 'Retry'; + String get debugEnterToken => 'Debug: Enter Plex Token'; + String get plexTokenLabel => 'Plex Auth Token'; + String get plexTokenHint => 'Enter your Plex.tv token'; + String get authenticationTimeout => + 'Authentication timed out. Please try again.'; + String get scanQRCodeInstruction => + 'Scan this QR code with a device logged into Plex to authenticate.'; + String get waitingForAuth => + 'Waiting for authentication...\nPlease complete sign-in in your browser.'; } // Path: common class _StringsCommonEn { - _StringsCommonEn._(this._root); + _StringsCommonEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get cancel => 'Cancel'; - String get save => 'Save'; - String get close => 'Close'; - String get clear => 'Clear'; - String get reset => 'Reset'; - String get later => 'Later'; - String get submit => 'Submit'; - String get confirm => 'Confirm'; - String get retry => 'Retry'; - String get playNow => 'Play Now'; - String get logout => 'Logout'; - String get online => 'Online'; - String get offline => 'Offline'; - String get owned => 'Owned'; - String get shared => 'Shared'; - String get current => 'CURRENT'; - String get unknown => 'Unknown'; - String get refresh => 'Refresh'; - String get yes => 'Yes'; - String get no => 'No'; - String get server => 'Server'; - String get delete => 'Delete'; - String get shuffle => 'Shuffle'; - String get addTo => 'Add to...'; + // Translations + String get cancel => 'Cancel'; + String get save => 'Save'; + String get close => 'Close'; + String get clear => 'Clear'; + String get reset => 'Reset'; + String get later => 'Later'; + String get submit => 'Submit'; + String get confirm => 'Confirm'; + String get retry => 'Retry'; + String get playNow => 'Play Now'; + String get logout => 'Logout'; + String get online => 'Online'; + String get offline => 'Offline'; + String get owned => 'Owned'; + String get shared => 'Shared'; + String get current => 'CURRENT'; + String get unknown => 'Unknown'; + String get refresh => 'Refresh'; + String get yes => 'Yes'; + String get no => 'No'; + String get server => 'Server'; + String get delete => 'Delete'; + String get shuffle => 'Shuffle'; + String get addTo => 'Add to...'; } // Path: screens class _StringsScreensEn { - _StringsScreensEn._(this._root); + _StringsScreensEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get licenses => 'Licenses'; - String get switchProfile => 'Switch Profile'; - String get subtitleStyling => 'Subtitle Styling'; - String get search => 'Search'; - String get logs => 'Logs'; + // Translations + String get licenses => 'Licenses'; + String get switchProfile => 'Switch Profile'; + String get subtitleStyling => 'Subtitle Styling'; + String get search => 'Search'; + String get logs => 'Logs'; } // Path: update class _StringsUpdateEn { - _StringsUpdateEn._(this._root); + _StringsUpdateEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get available => 'Update Available'; - String versionAvailable({required Object version}) => 'Version ${version} is available'; - String currentVersion({required Object version}) => 'Current: ${version}'; - String get skipVersion => 'Skip This Version'; - String get viewRelease => 'View Release'; - String get latestVersion => 'You are on the latest version'; - String get checkFailed => 'Failed to check for updates'; + // Translations + String get available => 'Update Available'; + String versionAvailable({required Object version}) => + 'Version ${version} is available'; + String currentVersion({required Object version}) => 'Current: ${version}'; + String get skipVersion => 'Skip This Version'; + String get viewRelease => 'View Release'; + String get latestVersion => 'You are on the latest version'; + String get checkFailed => 'Failed to check for updates'; } // Path: settings class _StringsSettingsEn { - _StringsSettingsEn._(this._root); + _StringsSettingsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Settings'; - String get language => 'Language'; - String get theme => 'Theme'; - String get appearance => 'Appearance'; - String get videoPlayback => 'Video Playback'; - String get shufflePlay => 'Shuffle Play'; - String get advanced => 'Advanced'; - String get useSeasonPostersDescription => 'Show season poster instead of series poster for episodes'; - String get showHeroSectionDescription => 'Display featured content carousel on home screen'; - String get secondsLabel => 'Seconds'; - String get minutesLabel => 'Minutes'; - String get secondsShort => 's'; - String get minutesShort => 'm'; - String durationHint({required Object min, required Object max}) => 'Enter duration (${min}-${max})'; - String get systemTheme => 'System'; - String get systemThemeDescription => 'Follow system settings'; - String get lightTheme => 'Light'; - String get darkTheme => 'Dark'; - String get libraryDensity => 'Library Density'; - String get compact => 'Compact'; - String get compactDescription => 'Smaller cards, more items visible'; - String get normal => 'Normal'; - String get normalDescription => 'Default size'; - String get comfortable => 'Comfortable'; - String get comfortableDescription => 'Larger cards, fewer items visible'; - String get viewMode => 'View Mode'; - String get gridView => 'Grid'; - String get gridViewDescription => 'Display items in a grid layout'; - String get listView => 'List'; - String get listViewDescription => 'Display items in a list layout'; - String get useSeasonPosters => 'Use Season Posters'; - String get showHeroSection => 'Show Hero Section'; - String get hardwareDecoding => 'Hardware Decoding'; - String get hardwareDecodingDescription => 'Use hardware acceleration when available'; - String get bufferSize => 'Buffer Size'; - String bufferSizeMB({required Object size}) => '${size}MB'; - String get subtitleStyling => 'Subtitle Styling'; - String get subtitleStylingDescription => 'Customize subtitle appearance'; - String get smallSkipDuration => 'Small Skip Duration'; - String get largeSkipDuration => 'Large Skip Duration'; - String secondsUnit({required Object seconds}) => '${seconds} seconds'; - String get defaultSleepTimer => 'Default Sleep Timer'; - String minutesUnit({required Object minutes}) => '${minutes} minutes'; - String get rememberTrackSelections => 'Remember track selections per show/movie'; - String get rememberTrackSelectionsDescription => 'Automatically save audio and subtitle language preferences when you change tracks during playback'; - String get unwatchedOnly => 'Unwatched Only'; - String get unwatchedOnlyDescription => 'Only include unwatched episodes in shuffle queue'; - String get shuffleOrderNavigation => 'Shuffle Order Navigation'; - String get shuffleOrderNavigationDescription => 'Next/previous buttons follow shuffled order'; - String get loopShuffleQueue => 'Loop Shuffle Queue'; - String get loopShuffleQueueDescription => 'Restart queue when reaching the end'; - String get videoPlayerControls => 'Video Player Controls'; - String get keyboardShortcuts => 'Keyboard Shortcuts'; - String get keyboardShortcutsDescription => 'Customize keyboard shortcuts'; - String get debugLogging => 'Debug Logging'; - String get debugLoggingDescription => 'Enable detailed logging for troubleshooting'; - String get viewLogs => 'View Logs'; - String get viewLogsDescription => 'View application logs'; - String get clearCache => 'Clear Cache'; - String get clearCacheDescription => 'This will clear all cached images and data. The app may take longer to load content after clearing the cache.'; - String get clearCacheSuccess => 'Cache cleared successfully'; - String get resetSettings => 'Reset Settings'; - String get resetSettingsDescription => 'This will reset all settings to their default values. This action cannot be undone.'; - String get resetSettingsSuccess => 'Settings reset successfully'; - String get shortcutsReset => 'Shortcuts reset to defaults'; - String get about => 'About'; - String get aboutDescription => 'App information and licenses'; - String get updates => 'Updates'; - String get updateAvailable => 'Update Available'; - String get checkForUpdates => 'Check for Updates'; - String get validationErrorEnterNumber => 'Please enter a valid number'; - String validationErrorDuration({required Object min, required Object max, required Object unit}) => 'Duration must be between ${min} and ${max} ${unit}'; - String shortcutAlreadyAssigned({required Object action}) => 'Shortcut already assigned to ${action}'; - String shortcutUpdated({required Object action}) => 'Shortcut updated for ${action}'; - String get autoSkip => 'Auto Skip'; - String get autoSkipIntro => 'Auto Skip Intro'; - String get autoSkipIntroDescription => 'Automatically skip intro markers after a few seconds'; - String get autoSkipCredits => 'Auto Skip Credits'; - String get autoSkipCreditsDescription => 'Automatically skip credits and play next episode'; - String get autoSkipDelay => 'Auto Skip Delay'; - String autoSkipDelayDescription({required Object seconds}) => 'Wait ${seconds} seconds before auto-skipping'; + // Translations + String get title => 'Settings'; + String get language => 'Language'; + String get theme => 'Theme'; + String get appearance => 'Appearance'; + String get videoPlayback => 'Video Playback'; + String get shufflePlay => 'Shuffle Play'; + String get advanced => 'Advanced'; + String get useSeasonPostersDescription => + 'Show season poster instead of series poster for episodes'; + String get showHeroSectionDescription => + 'Display featured content carousel on home screen'; + String get secondsLabel => 'Seconds'; + String get minutesLabel => 'Minutes'; + String get secondsShort => 's'; + String get minutesShort => 'm'; + String durationHint({required Object min, required Object max}) => + 'Enter duration (${min}-${max})'; + String get systemTheme => 'System'; + String get systemThemeDescription => 'Follow system settings'; + String get lightTheme => 'Light'; + String get darkTheme => 'Dark'; + String get libraryDensity => 'Library Density'; + String get compact => 'Compact'; + String get compactDescription => 'Smaller cards, more items visible'; + String get normal => 'Normal'; + String get normalDescription => 'Default size'; + String get comfortable => 'Comfortable'; + String get comfortableDescription => 'Larger cards, fewer items visible'; + String get viewMode => 'View Mode'; + String get gridView => 'Grid'; + String get gridViewDescription => 'Display items in a grid layout'; + String get listView => 'List'; + String get listViewDescription => 'Display items in a list layout'; + String get useSeasonPosters => 'Use Season Posters'; + String get showHeroSection => 'Show Hero Section'; + String get hardwareDecoding => 'Hardware Decoding'; + String get hardwareDecodingDescription => + 'Use hardware acceleration when available'; + String get bufferSize => 'Buffer Size'; + String bufferSizeMB({required Object size}) => '${size}MB'; + String get subtitleStyling => 'Subtitle Styling'; + String get subtitleStylingDescription => 'Customize subtitle appearance'; + String get smallSkipDuration => 'Small Skip Duration'; + String get largeSkipDuration => 'Large Skip Duration'; + String secondsUnit({required Object seconds}) => '${seconds} seconds'; + String get defaultSleepTimer => 'Default Sleep Timer'; + String minutesUnit({required Object minutes}) => '${minutes} minutes'; + String get rememberTrackSelections => + 'Remember track selections per show/movie'; + String get rememberTrackSelectionsDescription => + 'Automatically save audio and subtitle language preferences when you change tracks during playback'; + String get unwatchedOnly => 'Unwatched Only'; + String get unwatchedOnlyDescription => + 'Only include unwatched episodes in shuffle queue'; + String get shuffleOrderNavigation => 'Shuffle Order Navigation'; + String get shuffleOrderNavigationDescription => + 'Next/previous buttons follow shuffled order'; + String get loopShuffleQueue => 'Loop Shuffle Queue'; + String get loopShuffleQueueDescription => + 'Restart queue when reaching the end'; + String get videoPlayerControls => 'Video Player Controls'; + String get keyboardShortcuts => 'Keyboard Shortcuts'; + String get keyboardShortcutsDescription => 'Customize keyboard shortcuts'; + String get debugLogging => 'Debug Logging'; + String get debugLoggingDescription => + 'Enable detailed logging for troubleshooting'; + String get viewLogs => 'View Logs'; + String get viewLogsDescription => 'View application logs'; + String get clearCache => 'Clear Cache'; + String get clearCacheDescription => + 'This will clear all cached images and data. The app may take longer to load content after clearing the cache.'; + String get clearCacheSuccess => 'Cache cleared successfully'; + String get resetSettings => 'Reset Settings'; + String get resetSettingsDescription => + 'This will reset all settings to their default values. This action cannot be undone.'; + String get resetSettingsSuccess => 'Settings reset successfully'; + String get shortcutsReset => 'Shortcuts reset to defaults'; + String get about => 'About'; + String get aboutDescription => 'App information and licenses'; + String get updates => 'Updates'; + String get updateAvailable => 'Update Available'; + String get checkForUpdates => 'Check for Updates'; + String get validationErrorEnterNumber => 'Please enter a valid number'; + String validationErrorDuration({ + required Object min, + required Object max, + required Object unit, + }) => 'Duration must be between ${min} and ${max} ${unit}'; + String shortcutAlreadyAssigned({required Object action}) => + 'Shortcut already assigned to ${action}'; + String shortcutUpdated({required Object action}) => + 'Shortcut updated for ${action}'; + String get autoSkip => 'Auto Skip'; + String get autoSkipIntro => 'Auto Skip Intro'; + String get autoSkipIntroDescription => + 'Automatically skip intro markers after a few seconds'; + String get autoSkipCredits => 'Auto Skip Credits'; + String get autoSkipCreditsDescription => + 'Automatically skip credits and play next episode'; + String get autoSkipDelay => 'Auto Skip Delay'; + String autoSkipDelayDescription({required Object seconds}) => + 'Wait ${seconds} seconds before auto-skipping'; + String get downloads => 'Downloads'; + String get downloadLocationDescription => + 'Choose where to store downloaded content'; + String get downloadLocationDefault => 'Default (App Storage)'; + String get downloadLocationCustom => 'Custom Location'; + String get selectFolder => 'Select Folder'; + String get resetToDefault => 'Reset to Default'; + String currentPath({required Object path}) => 'Current: ${path}'; + String get downloadLocationNotSupported => + 'Custom download location is not supported on iOS'; + String get downloadLocationChanged => 'Download location changed'; + String get downloadLocationReset => 'Download location reset to default'; + String get downloadLocationInvalid => 'Selected folder is not writable'; + String get downloadLocationSelectError => 'Failed to select folder'; + String get downloadOnWifiOnly => 'Download on WiFi only'; + String get downloadOnWifiOnlyDescription => + 'Prevent downloads when on cellular data'; + String get cellularDownloadBlocked => + 'Downloads are disabled on cellular data. Connect to WiFi or change the setting.'; } // Path: search class _StringsSearchEn { - _StringsSearchEn._(this._root); + _StringsSearchEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get hint => 'Search movies, shows, music...'; - String get tryDifferentTerm => 'Try a different search term'; - String get searchYourMedia => 'Search your media'; - String get enterTitleActorOrKeyword => 'Enter a title, actor, or keyword'; + // Translations + String get hint => 'Search movies, shows, music...'; + String get tryDifferentTerm => 'Try a different search term'; + String get searchYourMedia => 'Search your media'; + String get enterTitleActorOrKeyword => 'Enter a title, actor, or keyword'; } // Path: hotkeys class _StringsHotkeysEn { - _StringsHotkeysEn._(this._root); + _StringsHotkeysEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String setShortcutFor({required Object actionName}) => 'Set Shortcut for ${actionName}'; - String get clearShortcut => 'Clear shortcut'; + // Translations + String setShortcutFor({required Object actionName}) => + 'Set Shortcut for ${actionName}'; + String get clearShortcut => 'Clear shortcut'; } // Path: pinEntry class _StringsPinEntryEn { - _StringsPinEntryEn._(this._root); + _StringsPinEntryEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get enterPin => 'Enter PIN'; - String get showPin => 'Show PIN'; - String get hidePin => 'Hide PIN'; + // Translations + String get enterPin => 'Enter PIN'; + String get showPin => 'Show PIN'; + String get hidePin => 'Hide PIN'; } // Path: fileInfo class _StringsFileInfoEn { - _StringsFileInfoEn._(this._root); + _StringsFileInfoEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'File Info'; - String get video => 'Video'; - String get audio => 'Audio'; - String get file => 'File'; - String get advanced => 'Advanced'; - String get codec => 'Codec'; - String get resolution => 'Resolution'; - String get bitrate => 'Bitrate'; - String get frameRate => 'Frame Rate'; - String get aspectRatio => 'Aspect Ratio'; - String get profile => 'Profile'; - String get bitDepth => 'Bit Depth'; - String get colorSpace => 'Color Space'; - String get colorRange => 'Color Range'; - String get colorPrimaries => 'Color Primaries'; - String get chromaSubsampling => 'Chroma Subsampling'; - String get channels => 'Channels'; - String get path => 'Path'; - String get size => 'Size'; - String get container => 'Container'; - String get duration => 'Duration'; - String get optimizedForStreaming => 'Optimized for Streaming'; - String get has64bitOffsets => '64-bit Offsets'; + // Translations + String get title => 'File Info'; + String get video => 'Video'; + String get audio => 'Audio'; + String get file => 'File'; + String get advanced => 'Advanced'; + String get codec => 'Codec'; + String get resolution => 'Resolution'; + String get bitrate => 'Bitrate'; + String get frameRate => 'Frame Rate'; + String get aspectRatio => 'Aspect Ratio'; + String get profile => 'Profile'; + String get bitDepth => 'Bit Depth'; + String get colorSpace => 'Color Space'; + String get colorRange => 'Color Range'; + String get colorPrimaries => 'Color Primaries'; + String get chromaSubsampling => 'Chroma Subsampling'; + String get channels => 'Channels'; + String get path => 'Path'; + String get size => 'Size'; + String get container => 'Container'; + String get duration => 'Duration'; + String get optimizedForStreaming => 'Optimized for Streaming'; + String get has64bitOffsets => '64-bit Offsets'; } // Path: mediaMenu class _StringsMediaMenuEn { - _StringsMediaMenuEn._(this._root); + _StringsMediaMenuEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get markAsWatched => 'Mark as Watched'; - String get markAsUnwatched => 'Mark as Unwatched'; - String get removeFromContinueWatching => 'Remove from Continue Watching'; - String get goToSeries => 'Go to series'; - String get goToSeason => 'Go to season'; - String get shufflePlay => 'Shuffle Play'; - String get fileInfo => 'File Info'; + // Translations + String get markAsWatched => 'Mark as Watched'; + String get markAsUnwatched => 'Mark as Unwatched'; + String get removeFromContinueWatching => 'Remove from Continue Watching'; + String get goToSeries => 'Go to series'; + String get goToSeason => 'Go to season'; + String get shufflePlay => 'Shuffle Play'; + String get fileInfo => 'File Info'; } // Path: accessibility class _StringsAccessibilityEn { - _StringsAccessibilityEn._(this._root); + _StringsAccessibilityEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String mediaCardMovie({required Object title}) => '${title}, movie'; - String mediaCardShow({required Object title}) => '${title}, TV show'; - String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - String get mediaCardWatched => 'watched'; - String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; - String get mediaCardUnwatched => 'unwatched'; - String get tapToPlay => 'Tap to play'; + // Translations + String mediaCardMovie({required Object title}) => '${title}, movie'; + String mediaCardShow({required Object title}) => '${title}, TV show'; + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + String get mediaCardWatched => 'watched'; + String mediaCardPartiallyWatched({required Object percent}) => + '${percent} percent watched'; + String get mediaCardUnwatched => 'unwatched'; + String get tapToPlay => 'Tap to play'; } // Path: tooltips class _StringsTooltipsEn { - _StringsTooltipsEn._(this._root); + _StringsTooltipsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get shufflePlay => 'Shuffle play'; - String get markAsWatched => 'Mark as watched'; - String get markAsUnwatched => 'Mark as unwatched'; + // Translations + String get shufflePlay => 'Shuffle play'; + String get markAsWatched => 'Mark as watched'; + String get markAsUnwatched => 'Mark as unwatched'; } // Path: videoControls class _StringsVideoControlsEn { - _StringsVideoControlsEn._(this._root); + _StringsVideoControlsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get audioLabel => 'Audio'; - String get subtitlesLabel => 'Subtitles'; - String get resetToZero => 'Reset to 0ms'; - String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - String playsLater({required Object label}) => '${label} plays later'; - String playsEarlier({required Object label}) => '${label} plays earlier'; - String get noOffset => 'No offset'; - String get letterbox => 'Letterbox'; - String get fillScreen => 'Fill screen'; - String get stretch => 'Stretch'; - String get lockRotation => 'Lock rotation'; - String get unlockRotation => 'Unlock rotation'; - String get sleepTimer => 'Sleep Timer'; - String get timerActive => 'Timer Active'; - String playbackWillPauseIn({required Object duration}) => 'Playback will pause in ${duration}'; - String get sleepTimerCompleted => 'Sleep timer completed - playback paused'; - String get playButton => 'Play'; - String get pauseButton => 'Pause'; - String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; - String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; - String get previousButton => 'Previous episode'; - String get nextButton => 'Next episode'; - String get previousChapterButton => 'Previous chapter'; - String get nextChapterButton => 'Next chapter'; - String get volumeButton => 'Volume'; - String get muteButton => 'Mute'; - String get unmuteButton => 'Unmute'; - String get settingsButton => 'Video settings'; - String get audioTrackButton => 'Audio tracks'; - String get subtitlesButton => 'Subtitles'; - String get chaptersButton => 'Chapters'; - String get versionsButton => 'Video versions'; - String get aspectRatioButton => 'Aspect ratio'; - String get fullscreenButton => 'Enter fullscreen'; - String get exitFullscreenButton => 'Exit fullscreen'; - String get rotationLockButton => 'Rotation lock'; - String get timelineSlider => 'Video timeline'; - String get volumeSlider => 'Volume level'; - String get backButton => 'Back'; + // Translations + String get audioLabel => 'Audio'; + String get subtitlesLabel => 'Subtitles'; + String get resetToZero => 'Reset to 0ms'; + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + String playsLater({required Object label}) => '${label} plays later'; + String playsEarlier({required Object label}) => '${label} plays earlier'; + String get noOffset => 'No offset'; + String get letterbox => 'Letterbox'; + String get fillScreen => 'Fill screen'; + String get stretch => 'Stretch'; + String get lockRotation => 'Lock rotation'; + String get unlockRotation => 'Unlock rotation'; + String get sleepTimer => 'Sleep Timer'; + String get timerActive => 'Timer Active'; + String playbackWillPauseIn({required Object duration}) => + 'Playback will pause in ${duration}'; + String get sleepTimerCompleted => 'Sleep timer completed - playback paused'; + String get playButton => 'Play'; + String get pauseButton => 'Pause'; + String seekBackwardButton({required Object seconds}) => + 'Seek backward ${seconds} seconds'; + String seekForwardButton({required Object seconds}) => + 'Seek forward ${seconds} seconds'; + String get previousButton => 'Previous episode'; + String get nextButton => 'Next episode'; + String get previousChapterButton => 'Previous chapter'; + String get nextChapterButton => 'Next chapter'; + String get volumeButton => 'Volume'; + String get muteButton => 'Mute'; + String get unmuteButton => 'Unmute'; + String get settingsButton => 'Video settings'; + String get audioTrackButton => 'Audio tracks'; + String get subtitlesButton => 'Subtitles'; + String get chaptersButton => 'Chapters'; + String get versionsButton => 'Video versions'; + String get aspectRatioButton => 'Aspect ratio'; + String get fullscreenButton => 'Enter fullscreen'; + String get exitFullscreenButton => 'Exit fullscreen'; + String get rotationLockButton => 'Rotation lock'; + String get timelineSlider => 'Video timeline'; + String get volumeSlider => 'Volume level'; + String get backButton => 'Back'; } // Path: userStatus class _StringsUserStatusEn { - _StringsUserStatusEn._(this._root); + _StringsUserStatusEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get admin => 'Admin'; - String get restricted => 'Restricted'; - String get protected => 'Protected'; - String get current => 'CURRENT'; + // Translations + String get admin => 'Admin'; + String get restricted => 'Restricted'; + String get protected => 'Protected'; + String get current => 'CURRENT'; } // Path: messages class _StringsMessagesEn { - _StringsMessagesEn._(this._root); + _StringsMessagesEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get markedAsWatched => 'Marked as watched'; - String get markedAsUnwatched => 'Marked as unwatched'; - String get removedFromContinueWatching => 'Removed from Continue Watching'; - String errorLoading({required Object error}) => 'Error: ${error}'; - String get fileInfoNotAvailable => 'File information not available'; - String errorLoadingFileInfo({required Object error}) => 'Error loading file info: ${error}'; - String get errorLoadingSeries => 'Error loading series'; - String get errorLoadingSeason => 'Error loading season'; - String get musicNotSupported => 'Music playback is not yet supported'; - String get logsCleared => 'Logs cleared'; - String get logsCopied => 'Logs copied to clipboard'; - String get noLogsAvailable => 'No logs available'; - String libraryScanning({required Object title}) => 'Scanning "${title}"...'; - String libraryScanStarted({required Object title}) => 'Library scan started for "${title}"'; - String libraryScanFailed({required Object error}) => 'Failed to scan library: ${error}'; - String metadataRefreshing({required Object title}) => 'Refreshing metadata for "${title}"...'; - String metadataRefreshStarted({required Object title}) => 'Metadata refresh started for "${title}"'; - String metadataRefreshFailed({required Object error}) => 'Failed to refresh metadata: ${error}'; - String get noPlexToken => 'No Plex token found. Please login again.'; - String get logoutConfirm => 'Are you sure you want to logout?'; - String get noSeasonsFound => 'No seasons found'; - String get noEpisodesFound => 'No episodes found in first season'; - String get noEpisodesFoundGeneral => 'No episodes found'; - String get noResultsFound => 'No results found'; - String sleepTimerSet({required Object label}) => 'Sleep timer set for ${label}'; - String failedToSwitchProfile({required Object displayName}) => 'Failed to switch to ${displayName}'; - String get noItemsAvailable => 'No items available'; - String get failedToCreatePlayQueue => 'Failed to create play queue'; - String get failedToCreatePlayQueueNoItems => 'Failed to create play queue - no items'; - String failedPlayback({required Object action, required Object error}) => 'Failed to ${action}: ${error}'; + // Translations + String get markedAsWatched => 'Marked as watched'; + String get markedAsUnwatched => 'Marked as unwatched'; + String get markedAsWatchedOffline => + 'Marked as watched (will sync when online)'; + String get markedAsUnwatchedOffline => + 'Marked as unwatched (will sync when online)'; + String get removedFromContinueWatching => 'Removed from Continue Watching'; + String errorLoading({required Object error}) => 'Error: ${error}'; + String get fileInfoNotAvailable => 'File information not available'; + String errorLoadingFileInfo({required Object error}) => + 'Error loading file info: ${error}'; + String get errorLoadingSeries => 'Error loading series'; + String get errorLoadingSeason => 'Error loading season'; + String get musicNotSupported => 'Music playback is not yet supported'; + String get logsCleared => 'Logs cleared'; + String get logsCopied => 'Logs copied to clipboard'; + String get noLogsAvailable => 'No logs available'; + String libraryScanning({required Object title}) => 'Scanning "${title}"...'; + String libraryScanStarted({required Object title}) => + 'Library scan started for "${title}"'; + String libraryScanFailed({required Object error}) => + 'Failed to scan library: ${error}'; + String metadataRefreshing({required Object title}) => + 'Refreshing metadata for "${title}"...'; + String metadataRefreshStarted({required Object title}) => + 'Metadata refresh started for "${title}"'; + String metadataRefreshFailed({required Object error}) => + 'Failed to refresh metadata: ${error}'; + String get noPlexToken => 'No Plex token found. Please login again.'; + String get logoutConfirm => 'Are you sure you want to logout?'; + String get noSeasonsFound => 'No seasons found'; + String get noEpisodesFound => 'No episodes found in first season'; + String get noEpisodesFoundGeneral => 'No episodes found'; + String get noResultsFound => 'No results found'; + String sleepTimerSet({required Object label}) => + 'Sleep timer set for ${label}'; + String failedToSwitchProfile({required Object displayName}) => + 'Failed to switch to ${displayName}'; + String get noItemsAvailable => 'No items available'; + String get failedToCreatePlayQueue => 'Failed to create play queue'; + String get failedToCreatePlayQueueNoItems => + 'Failed to create play queue - no items'; + String failedPlayback({required Object action, required Object error}) => + 'Failed to ${action}: ${error}'; + String get youAreOffline => 'You\'re Offline'; + String get offlineFeatureUnavailable => + 'This feature requires an internet connection. Your downloads and settings are still available.'; } // Path: profile class _StringsProfileEn { - _StringsProfileEn._(this._root); + _StringsProfileEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get noUsersAvailable => 'No users available'; + // Translations + String get noUsersAvailable => 'No users available'; } // Path: subtitlingStyling class _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingEn._(this._root); + _StringsSubtitlingStylingEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get stylingOptions => 'Styling Options'; - String get fontSize => 'Font Size'; - String get textColor => 'Text Color'; - String get borderSize => 'Border Size'; - String get borderColor => 'Border Color'; - String get backgroundOpacity => 'Background Opacity'; - String get backgroundColor => 'Background Color'; + // Translations + String get stylingOptions => 'Styling Options'; + String get fontSize => 'Font Size'; + String get textColor => 'Text Color'; + String get borderSize => 'Border Size'; + String get borderColor => 'Border Color'; + String get backgroundOpacity => 'Background Opacity'; + String get backgroundColor => 'Background Color'; } // Path: dialog class _StringsDialogEn { - _StringsDialogEn._(this._root); + _StringsDialogEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get confirmAction => 'Confirm Action'; - String get areYouSure => 'Are you sure you want to perform this action?'; - String get cancel => 'Cancel'; - String get playNow => 'Play Now'; + // Translations + String get confirmAction => 'Confirm Action'; + String get areYouSure => 'Are you sure you want to perform this action?'; + String get cancel => 'Cancel'; + String get playNow => 'Play Now'; } // Path: discover class _StringsDiscoverEn { - _StringsDiscoverEn._(this._root); + _StringsDiscoverEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Discover'; - String get switchProfile => 'Switch Profile'; - String get logout => 'Logout'; - String get noContentAvailable => 'No content available'; - String get addMediaToLibraries => 'Add some media to your libraries'; - String get continueWatching => 'Continue Watching'; - String get recentlyAdded => 'Recently Added'; - String get play => 'Play'; - String get resume => 'Resume'; - String playEpisode({required Object season, required Object episode}) => 'Play S${season}, E${episode}'; - String resumeEpisode({required Object season, required Object episode}) => 'Resume S${season}, E${episode}'; - String get pause => 'Pause'; - String get overview => 'Overview'; - String get cast => 'Cast'; - String get seasons => 'Seasons'; - String get studio => 'Studio'; - String get rating => 'Rating'; - String get watched => 'Watched'; - String episodeCount({required Object count}) => '${count} episodes'; - String watchedProgress({required Object watched, required Object total}) => '${watched}/${total} watched'; - String get movie => 'Movie'; - String get tvShow => 'TV Show'; - String minutesLeft({required Object minutes}) => '${minutes} min left'; + // Translations + String get title => 'Discover'; + String get switchProfile => 'Switch Profile'; + String get logout => 'Logout'; + String get noContentAvailable => 'No content available'; + String get addMediaToLibraries => 'Add some media to your libraries'; + String get continueWatching => 'Continue Watching'; + String get recentlyAdded => 'Recently Added'; + String get play => 'Play'; + String get resume => 'Resume'; + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + String get pause => 'Pause'; + String get overview => 'Overview'; + String get cast => 'Cast'; + String get seasons => 'Seasons'; + String get studio => 'Studio'; + String get rating => 'Rating'; + String get watched => 'Watched'; + String episodeCount({required Object count}) => '${count} episodes'; + String watchedProgress({required Object watched, required Object total}) => + '${watched}/${total} watched'; + String get movie => 'Movie'; + String get tvShow => 'TV Show'; + String minutesLeft({required Object minutes}) => '${minutes} min left'; } // Path: errors class _StringsErrorsEn { - _StringsErrorsEn._(this._root); + _StringsErrorsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String searchFailed({required Object error}) => 'Search failed: ${error}'; - String connectionTimeout({required Object context}) => 'Connection timeout while loading ${context}'; - String get connectionFailed => 'Unable to connect to Plex server'; - String failedToLoad({required Object context, required Object error}) => 'Failed to load ${context}: ${error}'; - String get noClientAvailable => 'No client available'; - String authenticationFailed({required Object error}) => 'Authentication failed: ${error}'; - String get couldNotLaunchUrl => 'Could not launch auth URL'; - String get pleaseEnterToken => 'Please enter a token'; - String get invalidToken => 'Invalid token'; - String failedToVerifyToken({required Object error}) => 'Failed to verify token: ${error}'; - String failedToSwitchProfile({required Object displayName}) => 'Failed to switch to ${displayName}'; - String get connectionFailedGeneric => 'Connection failed'; + // Translations + String searchFailed({required Object error}) => 'Search failed: ${error}'; + String connectionTimeout({required Object context}) => + 'Connection timeout while loading ${context}'; + String get connectionFailed => 'Unable to connect to Plex server'; + String failedToLoad({required Object context, required Object error}) => + 'Failed to load ${context}: ${error}'; + String get noClientAvailable => 'No client available'; + String authenticationFailed({required Object error}) => + 'Authentication failed: ${error}'; + String get couldNotLaunchUrl => 'Could not launch auth URL'; + String get pleaseEnterToken => 'Please enter a token'; + String get invalidToken => 'Invalid token'; + String failedToVerifyToken({required Object error}) => + 'Failed to verify token: ${error}'; + String failedToSwitchProfile({required Object displayName}) => + 'Failed to switch to ${displayName}'; + String get connectionFailedGeneric => 'Connection failed'; } // Path: libraries class _StringsLibrariesEn { - _StringsLibrariesEn._(this._root); + _StringsLibrariesEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Libraries'; - String get scanLibraryFiles => 'Scan Library Files'; - String get scanLibrary => 'Scan Library'; - String get analyze => 'Analyze'; - String get analyzeLibrary => 'Analyze Library'; - String get refreshMetadata => 'Refresh Metadata'; - String get emptyTrash => 'Empty Trash'; - String emptyingTrash({required Object title}) => 'Emptying trash for "${title}"...'; - String trashEmptied({required Object title}) => 'Trash emptied for "${title}"'; - String failedToEmptyTrash({required Object error}) => 'Failed to empty trash: ${error}'; - String analyzing({required Object title}) => 'Analyzing "${title}"...'; - String analysisStarted({required Object title}) => 'Analysis started for "${title}"'; - String failedToAnalyze({required Object error}) => 'Failed to analyze library: ${error}'; - String get noLibrariesFound => 'No libraries found'; - String get thisLibraryIsEmpty => 'This library is empty'; - String get all => 'All'; - String get clearAll => 'Clear All'; - String scanLibraryConfirm({required Object title}) => 'Are you sure you want to scan "${title}"?'; - String analyzeLibraryConfirm({required Object title}) => 'Are you sure you want to analyze "${title}"?'; - String refreshMetadataConfirm({required Object title}) => 'Are you sure you want to refresh metadata for "${title}"?'; - String emptyTrashConfirm({required Object title}) => 'Are you sure you want to empty trash for "${title}"?'; - String get manageLibraries => 'Manage Libraries'; - String get sort => 'Sort'; - String get sortBy => 'Sort By'; - String get filters => 'Filters'; - String loadingLibraryWithCount({required Object count}) => 'Loading library... (${count} items loaded)'; - String get confirmActionMessage => 'Are you sure you want to perform this action?'; - String get showLibrary => 'Show library'; - String get hideLibrary => 'Hide library'; - String get libraryOptions => 'Library options'; - String get content => 'library content'; - String get selectLibrary => 'Select library'; - String filtersWithCount({required Object count}) => 'Filters (${count})'; - String get noRecommendations => 'No recommendations available'; - String get noCollections => 'No collections in this library'; - String get noFoldersFound => 'No folders found'; - String get folders => 'folders'; - late final _StringsLibrariesTabsEn tabs = _StringsLibrariesTabsEn._(_root); - late final _StringsLibrariesGroupingsEn groupings = _StringsLibrariesGroupingsEn._(_root); + // Translations + String get title => 'Libraries'; + String get scanLibraryFiles => 'Scan Library Files'; + String get scanLibrary => 'Scan Library'; + String get analyze => 'Analyze'; + String get analyzeLibrary => 'Analyze Library'; + String get refreshMetadata => 'Refresh Metadata'; + String get emptyTrash => 'Empty Trash'; + String emptyingTrash({required Object title}) => + 'Emptying trash for "${title}"...'; + String trashEmptied({required Object title}) => + 'Trash emptied for "${title}"'; + String failedToEmptyTrash({required Object error}) => + 'Failed to empty trash: ${error}'; + String analyzing({required Object title}) => 'Analyzing "${title}"...'; + String analysisStarted({required Object title}) => + 'Analysis started for "${title}"'; + String failedToAnalyze({required Object error}) => + 'Failed to analyze library: ${error}'; + String get noLibrariesFound => 'No libraries found'; + String get thisLibraryIsEmpty => 'This library is empty'; + String get all => 'All'; + String get clearAll => 'Clear All'; + String scanLibraryConfirm({required Object title}) => + 'Are you sure you want to scan "${title}"?'; + String analyzeLibraryConfirm({required Object title}) => + 'Are you sure you want to analyze "${title}"?'; + String refreshMetadataConfirm({required Object title}) => + 'Are you sure you want to refresh metadata for "${title}"?'; + String emptyTrashConfirm({required Object title}) => + 'Are you sure you want to empty trash for "${title}"?'; + String get manageLibraries => 'Manage Libraries'; + String get sort => 'Sort'; + String get sortBy => 'Sort By'; + String get filters => 'Filters'; + String loadingLibraryWithCount({required Object count}) => + 'Loading library... (${count} items loaded)'; + String get confirmActionMessage => + 'Are you sure you want to perform this action?'; + String get showLibrary => 'Show library'; + String get hideLibrary => 'Hide library'; + String get libraryOptions => 'Library options'; + String get content => 'library content'; + String get selectLibrary => 'Select library'; + String filtersWithCount({required Object count}) => 'Filters (${count})'; + String get noRecommendations => 'No recommendations available'; + String get noCollections => 'No collections in this library'; + String get noFoldersFound => 'No folders found'; + String get folders => 'folders'; + late final _StringsLibrariesTabsEn tabs = _StringsLibrariesTabsEn._(_root); + late final _StringsLibrariesGroupingsEn groupings = + _StringsLibrariesGroupingsEn._(_root); } // Path: about class _StringsAboutEn { - _StringsAboutEn._(this._root); + _StringsAboutEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'About'; - String get openSourceLicenses => 'Open Source Licenses'; - String versionLabel({required Object version}) => 'Version ${version}'; - String get appDescription => 'A beautiful Plex client for Flutter'; - String get viewLicensesDescription => 'View licenses of third-party libraries'; + // Translations + String get title => 'About'; + String get openSourceLicenses => 'Open Source Licenses'; + String versionLabel({required Object version}) => 'Version ${version}'; + String get appDescription => 'A beautiful Plex client for Flutter'; + String get viewLicensesDescription => + 'View licenses of third-party libraries'; } // Path: serverSelection class _StringsServerSelectionEn { - _StringsServerSelectionEn._(this._root); + _StringsServerSelectionEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get connectingToServer => 'Connecting to server...'; - String get connectingToAllServers => 'Connecting to all servers...'; - String get connectToAllServers => 'Connect to All Servers'; - String get allServerConnectionsFailed => 'Failed to connect to any servers. Please check your network and try again.'; - String foundServers({required Object count}) => 'Found ${count} server(s)'; - String get orSelectIndividually => 'Or select a server individually:'; - String get serverDebugCopied => 'Server debug data copied to clipboard'; - String get copyDebugData => 'Copy Debug Data'; - String get noServersFound => 'No servers found'; - String noServersFoundForAccount({required Object username, required Object email}) => 'No servers found for ${username} (${email})'; - String malformedServerData({required Object count}) => 'Found ${count} server(s) with malformed data. No valid servers available.'; - String get incompleteServerInfo => 'Some servers have incomplete information and were skipped. Please check your Plex.tv account.'; - String get incompleteConnectionInfo => 'Server connection information is incomplete. Please try again.'; - String malformedServerInfo({required Object message}) => 'Server information is malformed: ${message}'; - String get networkConnectionFailed => 'Network connection failed. Please check your internet connection and try again.'; - String get authenticationFailed => 'Authentication failed. Please sign in again.'; - String get plexServiceUnavailable => 'Plex service unavailable. Please try again later.'; - String failedToLoadServers({required Object error}) => 'Failed to load servers: ${error}'; + // Translations + String get connectingToServer => 'Connecting to server...'; + String get connectingToAllServers => 'Connecting to all servers...'; + String get connectToAllServers => 'Connect to All Servers'; + String get allServerConnectionsFailed => + 'Failed to connect to any servers. Please check your network and try again.'; + String foundServers({required Object count}) => 'Found ${count} server(s)'; + String get orSelectIndividually => 'Or select a server individually:'; + String get serverDebugCopied => 'Server debug data copied to clipboard'; + String get copyDebugData => 'Copy Debug Data'; + String get noServersFound => 'No servers found'; + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => 'No servers found for ${username} (${email})'; + String malformedServerData({required Object count}) => + 'Found ${count} server(s) with malformed data. No valid servers available.'; + String get incompleteServerInfo => + 'Some servers have incomplete information and were skipped. Please check your Plex.tv account.'; + String get incompleteConnectionInfo => + 'Server connection information is incomplete. Please try again.'; + String malformedServerInfo({required Object message}) => + 'Server information is malformed: ${message}'; + String get networkConnectionFailed => + 'Network connection failed. Please check your internet connection and try again.'; + String get authenticationFailed => + 'Authentication failed. Please sign in again.'; + String get plexServiceUnavailable => + 'Plex service unavailable. Please try again later.'; + String failedToLoadServers({required Object error}) => + 'Failed to load servers: ${error}'; } // Path: hubDetail class _StringsHubDetailEn { - _StringsHubDetailEn._(this._root); + _StringsHubDetailEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Title'; - String get releaseYear => 'Release Year'; - String get dateAdded => 'Date Added'; - String get rating => 'Rating'; - String get noItemsFound => 'No items found'; + // Translations + String get title => 'Title'; + String get releaseYear => 'Release Year'; + String get dateAdded => 'Date Added'; + String get rating => 'Rating'; + String get noItemsFound => 'No items found'; } // Path: logs class _StringsLogsEn { - _StringsLogsEn._(this._root); + _StringsLogsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Logs'; - String get clearLogs => 'Clear Logs'; - String get copyLogs => 'Copy Logs'; - String get exportLogs => 'Export Logs'; - String get noLogsToShow => 'No logs to show'; - String get error => 'Error:'; - String get stackTrace => 'Stack Trace:'; + // Translations + String get title => 'Logs'; + String get clearLogs => 'Clear Logs'; + String get copyLogs => 'Copy Logs'; + String get exportLogs => 'Export Logs'; + String get noLogsToShow => 'No logs to show'; + String get error => 'Error:'; + String get stackTrace => 'Stack Trace:'; } // Path: licenses class _StringsLicensesEn { - _StringsLicensesEn._(this._root); + _StringsLicensesEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get relatedPackages => 'Related Packages'; - String get license => 'License'; - String licenseNumber({required Object number}) => 'License ${number}'; - String licensesCount({required Object count}) => '${count} licenses'; + // Translations + String get relatedPackages => 'Related Packages'; + String get license => 'License'; + String licenseNumber({required Object number}) => 'License ${number}'; + String licensesCount({required Object count}) => '${count} licenses'; } // Path: navigation class _StringsNavigationEn { - _StringsNavigationEn._(this._root); + _StringsNavigationEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get home => 'Home'; - String get search => 'Search'; - String get libraries => 'Libraries'; - String get settings => 'Settings'; + // Translations + String get home => 'Home'; + String get search => 'Search'; + String get libraries => 'Libraries'; + String get settings => 'Settings'; + String get downloads => 'Downloads'; } // Path: collections class _StringsCollectionsEn { - _StringsCollectionsEn._(this._root); + _StringsCollectionsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Collections'; - String get collection => 'Collection'; - String get empty => 'Collection is empty'; - String get noItems => 'No items in this collection'; - String get unknownLibrarySection => 'Cannot delete: Unknown library section'; - String get deleteCollection => 'Delete Collection'; - String deleteConfirm({required Object title}) => 'Are you sure you want to delete "${title}"? This action cannot be undone.'; - String get deleted => 'Collection deleted'; - String get deleteFailed => 'Failed to delete collection'; - String deleteFailedWithError({required Object error}) => 'Failed to delete collection: ${error}'; - String failedToLoadItems({required Object error}) => 'Failed to load collection items: ${error}'; - String get addTo => 'Add to collection'; - String get selectCollection => 'Select Collection'; - String get createNewCollection => 'Create New Collection'; - String get collectionName => 'Collection Name'; - String get enterCollectionName => 'Enter collection name'; - String get addedToCollection => 'Added to collection'; - String get errorAddingToCollection => 'Failed to add to collection'; - String get created => 'Collection created'; - String get removeFromCollection => 'Remove from collection'; - String removeFromCollectionConfirm({required Object title}) => 'Remove "${title}" from this collection?'; - String get removedFromCollection => 'Removed from collection'; - String get removeFromCollectionFailed => 'Failed to remove from collection'; - String removeFromCollectionError({required Object error}) => 'Error removing from collection: ${error}'; + // Translations + String get title => 'Collections'; + String get collection => 'Collection'; + String get empty => 'Collection is empty'; + String get noItems => 'No items in this collection'; + String get unknownLibrarySection => 'Cannot delete: Unknown library section'; + String get deleteCollection => 'Delete Collection'; + String deleteConfirm({required Object title}) => + 'Are you sure you want to delete "${title}"? This action cannot be undone.'; + String get deleted => 'Collection deleted'; + String get deleteFailed => 'Failed to delete collection'; + String deleteFailedWithError({required Object error}) => + 'Failed to delete collection: ${error}'; + String failedToLoadItems({required Object error}) => + 'Failed to load collection items: ${error}'; + String get addTo => 'Add to collection'; + String get selectCollection => 'Select Collection'; + String get createNewCollection => 'Create New Collection'; + String get collectionName => 'Collection Name'; + String get enterCollectionName => 'Enter collection name'; + String get addedToCollection => 'Added to collection'; + String get errorAddingToCollection => 'Failed to add to collection'; + String get created => 'Collection created'; + String get removeFromCollection => 'Remove from collection'; + String removeFromCollectionConfirm({required Object title}) => + 'Remove "${title}" from this collection?'; + String get removedFromCollection => 'Removed from collection'; + String get removeFromCollectionFailed => 'Failed to remove from collection'; + String removeFromCollectionError({required Object error}) => + 'Error removing from collection: ${error}'; } // Path: playlists class _StringsPlaylistsEn { - _StringsPlaylistsEn._(this._root); + _StringsPlaylistsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get title => 'Playlists'; - String get playlist => 'Playlist'; - String get noPlaylists => 'No playlists found'; - String get create => 'Create Playlist'; - String get newPlaylist => 'New Playlist'; - String get playlistName => 'Playlist Name'; - String get enterPlaylistName => 'Enter playlist name'; - String get edit => 'Edit Playlist'; - String get delete => 'Delete Playlist'; - String get addTo => 'Add to Playlist'; - String get addItems => 'Add Items'; - String get removeItem => 'Remove from Playlist'; - String get clearPlaylist => 'Clear Playlist'; - String get playAll => 'Play All'; - String get shuffle => 'Shuffle'; - String get smartPlaylist => 'Smart Playlist'; - String get regularPlaylist => 'Regular Playlist'; - String itemCount({required Object count}) => '${count} items'; - String get oneItem => '1 item'; - String get emptyPlaylist => 'This playlist is empty'; - String get deleteConfirm => 'Delete Playlist?'; - String deleteMessage({required Object name}) => 'Are you sure you want to delete "${name}"?'; - String get created => 'Playlist created'; - String get updated => 'Playlist updated'; - String get deleted => 'Playlist deleted'; - String get itemAdded => 'Added to playlist'; - String get itemRemoved => 'Removed from playlist'; - String get selectPlaylist => 'Select Playlist'; - String get createNewPlaylist => 'Create New Playlist'; - String get errorCreating => 'Failed to create playlist'; - String get errorDeleting => 'Failed to delete playlist'; - String get errorLoading => 'Failed to load playlists'; - String get errorAdding => 'Failed to add to playlist'; - String get errorReordering => 'Failed to reorder playlist item'; - String get errorRemoving => 'Failed to remove from playlist'; + // Translations + String get title => 'Playlists'; + String get playlist => 'Playlist'; + String get noPlaylists => 'No playlists found'; + String get create => 'Create Playlist'; + String get newPlaylist => 'New Playlist'; + String get playlistName => 'Playlist Name'; + String get enterPlaylistName => 'Enter playlist name'; + String get edit => 'Edit Playlist'; + String get delete => 'Delete Playlist'; + String get addTo => 'Add to Playlist'; + String get addItems => 'Add Items'; + String get removeItem => 'Remove from Playlist'; + String get clearPlaylist => 'Clear Playlist'; + String get playAll => 'Play All'; + String get shuffle => 'Shuffle'; + String get smartPlaylist => 'Smart Playlist'; + String get regularPlaylist => 'Regular Playlist'; + String itemCount({required Object count}) => '${count} items'; + String get oneItem => '1 item'; + String get emptyPlaylist => 'This playlist is empty'; + String get deleteConfirm => 'Delete Playlist?'; + String deleteMessage({required Object name}) => + 'Are you sure you want to delete "${name}"?'; + String get created => 'Playlist created'; + String get updated => 'Playlist updated'; + String get deleted => 'Playlist deleted'; + String get itemAdded => 'Added to playlist'; + String get itemRemoved => 'Removed from playlist'; + String get selectPlaylist => 'Select Playlist'; + String get createNewPlaylist => 'Create New Playlist'; + String get errorCreating => 'Failed to create playlist'; + String get errorDeleting => 'Failed to delete playlist'; + String get errorLoading => 'Failed to load playlists'; + String get errorAdding => 'Failed to add to playlist'; + String get errorReordering => 'Failed to reorder playlist item'; + String get errorRemoving => 'Failed to remove from playlist'; +} + +// Path: downloads +class _StringsDownloadsEn { + _StringsDownloadsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Downloads'; + String get manage => 'Manage'; + String get tvShows => 'TV Shows'; + String get movies => 'Movies'; + String get queue => 'Queue'; + String get noDownloads => 'No downloads yet'; + String get noDownloadsDescription => + 'Downloaded content will appear here for offline viewing'; + String get browseContent => 'Browse content'; + String get downloading => 'Downloading'; + String get downloaded => 'Downloaded'; + String get paused => 'Paused'; + String get failed => 'Failed'; + String get queued => 'Queued'; + String get downloadNow => 'Download'; + String get deleteDownload => 'Delete download'; + String get cancelDownload => 'Cancel download'; + String get pauseDownload => 'Pause download'; + String get resumeDownload => 'Resume download'; + String get retryDownload => 'Retry download'; + String get downloadComplete => 'Download complete'; + String get downloadFailed => 'Download failed'; + String get downloadCancelled => 'Download cancelled'; + String get downloadQueued => 'Download queued'; + String episodesQueued({required Object count}) => + '${count} episodes queued for download'; + String get downloadDeleted => 'Download deleted'; + String get deleteConfirmTitle => 'Delete download?'; + String deleteConfirm({required Object title}) => + 'Are you sure you want to delete "${title}"? This will remove the downloaded file from your device.'; + String get deleteConfirmMessage => + 'This will remove the downloaded file from your device.'; + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => 'Deleting ${title}... (${current} of ${total})'; + String deletingSimple({required Object title}) => 'Deleting ${title}...'; + String get storageUsed => 'Storage used'; + String get availableOffline => 'Available offline'; + String get downloadingSeason => 'Downloading season'; + String get downloadingEpisode => 'Downloading episode'; +} + +// Path: offline +class _StringsOfflineEn { + _StringsOfflineEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get youAreOffline => 'You\'re offline'; + String get offlineMode => 'Offline mode'; + String get backOnline => 'Back online'; + String get showingDownloadedContent => 'Showing downloaded content only'; + String get noInternetConnection => 'No internet connection'; + String get serversUnreachable => 'Plex servers unreachable'; + String get connectToDownload => 'Connect to the internet to download content'; } // Path: libraries.tabs class _StringsLibrariesTabsEn { - _StringsLibrariesTabsEn._(this._root); + _StringsLibrariesTabsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get recommended => 'Recommended'; - String get browse => 'Browse'; - String get collections => 'Collections'; - String get playlists => 'Playlists'; + // Translations + String get recommended => 'Recommended'; + String get browse => 'Browse'; + String get collections => 'Collections'; + String get playlists => 'Playlists'; } // Path: libraries.groupings class _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsEn._(this._root); + _StringsLibrariesGroupingsEn._(this._root); - final Translations _root; // ignore: unused_field + final Translations _root; // ignore: unused_field - // Translations - String get all => 'All'; - String get movies => 'Movies'; - String get shows => 'TV Shows'; - String get seasons => 'Seasons'; - String get episodes => 'Episodes'; - String get folders => 'Folders'; + // Translations + String get all => 'All'; + String get movies => 'Movies'; + String get shows => 'TV Shows'; + String get seasons => 'Seasons'; + String get episodes => 'Episodes'; + String get folders => 'Folders'; } // Path: class _StringsDe implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsDe.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.de, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsDe.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); - @override late final _StringsDe _root = this; // ignore: unused_field + @override + late final _StringsDe _root = this; // ignore: unused_field - // Translations - @override late final _StringsAppDe app = _StringsAppDe._(_root); - @override late final _StringsAuthDe auth = _StringsAuthDe._(_root); - @override late final _StringsCommonDe common = _StringsCommonDe._(_root); - @override late final _StringsScreensDe screens = _StringsScreensDe._(_root); - @override late final _StringsUpdateDe update = _StringsUpdateDe._(_root); - @override late final _StringsSettingsDe settings = _StringsSettingsDe._(_root); - @override late final _StringsSearchDe search = _StringsSearchDe._(_root); - @override late final _StringsHotkeysDe hotkeys = _StringsHotkeysDe._(_root); - @override late final _StringsPinEntryDe pinEntry = _StringsPinEntryDe._(_root); - @override late final _StringsFileInfoDe fileInfo = _StringsFileInfoDe._(_root); - @override late final _StringsMediaMenuDe mediaMenu = _StringsMediaMenuDe._(_root); - @override late final _StringsAccessibilityDe accessibility = _StringsAccessibilityDe._(_root); - @override late final _StringsTooltipsDe tooltips = _StringsTooltipsDe._(_root); - @override late final _StringsVideoControlsDe videoControls = _StringsVideoControlsDe._(_root); - @override late final _StringsUserStatusDe userStatus = _StringsUserStatusDe._(_root); - @override late final _StringsMessagesDe messages = _StringsMessagesDe._(_root); - @override late final _StringsProfileDe profile = _StringsProfileDe._(_root); - @override late final _StringsSubtitlingStylingDe subtitlingStyling = _StringsSubtitlingStylingDe._(_root); - @override late final _StringsDialogDe dialog = _StringsDialogDe._(_root); - @override late final _StringsDiscoverDe discover = _StringsDiscoverDe._(_root); - @override late final _StringsErrorsDe errors = _StringsErrorsDe._(_root); - @override late final _StringsLibrariesDe libraries = _StringsLibrariesDe._(_root); - @override late final _StringsAboutDe about = _StringsAboutDe._(_root); - @override late final _StringsServerSelectionDe serverSelection = _StringsServerSelectionDe._(_root); - @override late final _StringsHubDetailDe hubDetail = _StringsHubDetailDe._(_root); - @override late final _StringsLogsDe logs = _StringsLogsDe._(_root); - @override late final _StringsLicensesDe licenses = _StringsLicensesDe._(_root); - @override late final _StringsNavigationDe navigation = _StringsNavigationDe._(_root); - @override late final _StringsPlaylistsDe playlists = _StringsPlaylistsDe._(_root); - @override late final _StringsCollectionsDe collections = _StringsCollectionsDe._(_root); + // Translations + @override + late final _StringsAppDe app = _StringsAppDe._(_root); + @override + late final _StringsAuthDe auth = _StringsAuthDe._(_root); + @override + late final _StringsCommonDe common = _StringsCommonDe._(_root); + @override + late final _StringsScreensDe screens = _StringsScreensDe._(_root); + @override + late final _StringsUpdateDe update = _StringsUpdateDe._(_root); + @override + late final _StringsSettingsDe settings = _StringsSettingsDe._(_root); + @override + late final _StringsSearchDe search = _StringsSearchDe._(_root); + @override + late final _StringsHotkeysDe hotkeys = _StringsHotkeysDe._(_root); + @override + late final _StringsPinEntryDe pinEntry = _StringsPinEntryDe._(_root); + @override + late final _StringsFileInfoDe fileInfo = _StringsFileInfoDe._(_root); + @override + late final _StringsMediaMenuDe mediaMenu = _StringsMediaMenuDe._(_root); + @override + late final _StringsAccessibilityDe accessibility = _StringsAccessibilityDe._( + _root, + ); + @override + late final _StringsTooltipsDe tooltips = _StringsTooltipsDe._(_root); + @override + late final _StringsVideoControlsDe videoControls = _StringsVideoControlsDe._( + _root, + ); + @override + late final _StringsUserStatusDe userStatus = _StringsUserStatusDe._(_root); + @override + late final _StringsMessagesDe messages = _StringsMessagesDe._(_root); + @override + late final _StringsProfileDe profile = _StringsProfileDe._(_root); + @override + late final _StringsSubtitlingStylingDe subtitlingStyling = + _StringsSubtitlingStylingDe._(_root); + @override + late final _StringsDialogDe dialog = _StringsDialogDe._(_root); + @override + late final _StringsDiscoverDe discover = _StringsDiscoverDe._(_root); + @override + late final _StringsErrorsDe errors = _StringsErrorsDe._(_root); + @override + late final _StringsLibrariesDe libraries = _StringsLibrariesDe._(_root); + @override + late final _StringsAboutDe about = _StringsAboutDe._(_root); + @override + late final _StringsServerSelectionDe serverSelection = + _StringsServerSelectionDe._(_root); + @override + late final _StringsHubDetailDe hubDetail = _StringsHubDetailDe._(_root); + @override + late final _StringsLogsDe logs = _StringsLogsDe._(_root); + @override + late final _StringsLicensesDe licenses = _StringsLicensesDe._(_root); + @override + late final _StringsNavigationDe navigation = _StringsNavigationDe._(_root); + @override + late final _StringsDownloadsDe downloads = _StringsDownloadsDe._(_root); + @override + late final _StringsOfflineDe offline = _StringsOfflineDe._(_root); + @override + late final _StringsPlaylistsDe playlists = _StringsPlaylistsDe._(_root); + @override + late final _StringsCollectionsDe collections = _StringsCollectionsDe._(_root); } // Path: app class _StringsAppDe implements _StringsAppEn { - _StringsAppDe._(this._root); + _StringsAppDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Plezy'; - @override String get loading => 'Lädt...'; + // Translations + @override + String get title => 'Plezy'; + @override + String get loading => 'Lädt...'; } // Path: auth class _StringsAuthDe implements _StringsAuthEn { - _StringsAuthDe._(this._root); + _StringsAuthDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get signInWithPlex => 'Mit Plex anmelden'; - @override String get showQRCode => 'QR-Code anzeigen'; - @override String get cancel => 'Abbrechen'; - @override String get authenticate => 'Authentifizieren'; - @override String get retry => 'Erneut versuchen'; - @override String get debugEnterToken => 'Debug: Plex-Token eingeben'; - @override String get plexTokenLabel => 'Plex-Auth-Token'; - @override String get plexTokenHint => 'Plex.tv-Token eingeben'; - @override String get authenticationTimeout => 'Authentifizierung abgelaufen. Bitte erneut versuchen.'; - @override String get scanQRCodeInstruction => 'Diesen QR-Code mit einem bei Plex angemeldeten Gerät scannen, um zu authentifizieren.'; - @override String get waitingForAuth => 'Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.'; + // Translations + @override + String get signInWithPlex => 'Mit Plex anmelden'; + @override + String get showQRCode => 'QR-Code anzeigen'; + @override + String get cancel => 'Abbrechen'; + @override + String get authenticate => 'Authentifizieren'; + @override + String get retry => 'Erneut versuchen'; + @override + String get debugEnterToken => 'Debug: Plex-Token eingeben'; + @override + String get plexTokenLabel => 'Plex-Auth-Token'; + @override + String get plexTokenHint => 'Plex.tv-Token eingeben'; + @override + String get authenticationTimeout => + 'Authentifizierung abgelaufen. Bitte erneut versuchen.'; + @override + String get scanQRCodeInstruction => + 'Diesen QR-Code mit einem bei Plex angemeldeten Gerät scannen, um zu authentifizieren.'; + @override + String get waitingForAuth => + 'Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.'; } // Path: common class _StringsCommonDe implements _StringsCommonEn { - _StringsCommonDe._(this._root); + _StringsCommonDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get cancel => 'Abbrechen'; - @override String get save => 'Speichern'; - @override String get close => 'Schließen'; - @override String get clear => 'Leeren'; - @override String get reset => 'Zurücksetzen'; - @override String get later => 'Später'; - @override String get submit => 'Senden'; - @override String get confirm => 'Bestätigen'; - @override String get retry => 'Erneut versuchen'; - @override String get playNow => 'Jetzt abspielen'; - @override String get logout => 'Abmelden'; - @override String get online => 'Online'; - @override String get offline => 'Offline'; - @override String get owned => 'Besitzer'; - @override String get shared => 'Geteilt'; - @override String get current => 'AKTUELL'; - @override String get unknown => 'Unbekannt'; - @override String get refresh => 'Aktualisieren'; - @override String get yes => 'Ja'; - @override String get no => 'Nein'; - @override String get server => 'Server'; - @override String get delete => 'Löschen'; - @override String get shuffle => 'Zufall'; - @override String get addTo => 'Hinzufügen zu...'; + // Translations + @override + String get cancel => 'Abbrechen'; + @override + String get save => 'Speichern'; + @override + String get close => 'Schließen'; + @override + String get clear => 'Leeren'; + @override + String get reset => 'Zurücksetzen'; + @override + String get later => 'Später'; + @override + String get submit => 'Senden'; + @override + String get confirm => 'Bestätigen'; + @override + String get retry => 'Erneut versuchen'; + @override + String get playNow => 'Jetzt abspielen'; + @override + String get logout => 'Abmelden'; + @override + String get online => 'Online'; + @override + String get offline => 'Offline'; + @override + String get owned => 'Besitzer'; + @override + String get shared => 'Geteilt'; + @override + String get current => 'AKTUELL'; + @override + String get unknown => 'Unbekannt'; + @override + String get refresh => 'Aktualisieren'; + @override + String get yes => 'Ja'; + @override + String get no => 'Nein'; + @override + String get server => 'Server'; + @override + String get delete => 'Löschen'; + @override + String get shuffle => 'Zufall'; + @override + String get addTo => 'Hinzufügen zu...'; } // Path: screens class _StringsScreensDe implements _StringsScreensEn { - _StringsScreensDe._(this._root); + _StringsScreensDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get licenses => 'Lizenzen'; - @override String get selectServer => 'Server auswählen'; - @override String get switchProfile => 'Profil wechseln'; - @override String get subtitleStyling => 'Untertitel-Stil'; - @override String get search => 'Suche'; - @override String get logs => 'Protokolle'; + // Translations + @override + String get licenses => 'Lizenzen'; + @override + String get selectServer => 'Server auswählen'; + @override + String get switchProfile => 'Profil wechseln'; + @override + String get subtitleStyling => 'Untertitel-Stil'; + @override + String get search => 'Suche'; + @override + String get logs => 'Protokolle'; } // Path: update class _StringsUpdateDe implements _StringsUpdateEn { - _StringsUpdateDe._(this._root); + _StringsUpdateDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get available => 'Update verfügbar'; - @override String versionAvailable({required Object version}) => 'Version ${version} ist verfügbar'; - @override String currentVersion({required Object version}) => 'Aktuell: ${version}'; - @override String get skipVersion => 'Diese Version überspringen'; - @override String get viewRelease => 'Release anzeigen'; - @override String get latestVersion => 'Aktuellste Version installiert'; - @override String get checkFailed => 'Fehler bei der Updateprüfung'; + // Translations + @override + String get available => 'Update verfügbar'; + @override + String versionAvailable({required Object version}) => + 'Version ${version} ist verfügbar'; + @override + String currentVersion({required Object version}) => 'Aktuell: ${version}'; + @override + String get skipVersion => 'Diese Version überspringen'; + @override + String get viewRelease => 'Release anzeigen'; + @override + String get latestVersion => 'Aktuellste Version installiert'; + @override + String get checkFailed => 'Fehler bei der Updateprüfung'; } // Path: settings class _StringsSettingsDe implements _StringsSettingsEn { - _StringsSettingsDe._(this._root); + _StringsSettingsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Einstellungen'; - @override String get language => 'Sprache'; - @override String get theme => 'Design'; - @override String get appearance => 'Darstellung'; - @override String get videoPlayback => 'Videowiedergabe'; - @override String get shufflePlay => 'Zufallswiedergabe'; - @override String get advanced => 'Erweitert'; - @override String get useSeasonPostersDescription => 'Staffelposter statt Serienposter für Episoden anzeigen'; - @override String get showHeroSectionDescription => 'Bereich mit empfohlenen Inhalten auf der Startseite anzeigen'; - @override String get secondsLabel => 'Sekunden'; - @override String get minutesLabel => 'Minuten'; - @override String get secondsShort => 's'; - @override String get minutesShort => 'm'; - @override String durationHint({required Object min, required Object max}) => 'Dauer eingeben (${min}-${max})'; - @override String get systemTheme => 'System'; - @override String get systemThemeDescription => 'Systemeinstellungen folgen'; - @override String get lightTheme => 'Hell'; - @override String get darkTheme => 'Dunkel'; - @override String get libraryDensity => 'Mediathekdichte'; - @override String get compact => 'Kompakt'; - @override String get compactDescription => 'Kleinere Karten, mehr Elemente sichtbar'; - @override String get normal => 'Normal'; - @override String get normalDescription => 'Standardgröße'; - @override String get comfortable => 'Großzügig'; - @override String get comfortableDescription => 'Größere Karten, weniger Elemente sichtbar'; - @override String get viewMode => 'Ansichtsmodus'; - @override String get gridView => 'Raster'; - @override String get gridViewDescription => 'Elemente im Raster anzeigen'; - @override String get listView => 'Liste'; - @override String get listViewDescription => 'Elemente in Listenansicht anzeigen'; - @override String get useSeasonPosters => 'Staffelposter verwenden'; - @override String get showHeroSection => 'Hero-Bereich anzeigen'; - @override String get hardwareDecoding => 'Hardware-Decodierung'; - @override String get hardwareDecodingDescription => 'Hardwarebeschleunigung verwenden, sofern verfügbar'; - @override String get bufferSize => 'Puffergröße'; - @override String bufferSizeMB({required Object size}) => '${size}MB'; - @override String get subtitleStyling => 'Untertitel-Stil'; - @override String get subtitleStylingDescription => 'Aussehen von Untertiteln anpassen'; - @override String get smallSkipDuration => 'Kleine Sprungdauer'; - @override String get largeSkipDuration => 'Große Sprungdauer'; - @override String secondsUnit({required Object seconds}) => '${seconds} Sekunden'; - @override String get defaultSleepTimer => 'Standard-Sleep-Timer'; - @override String minutesUnit({required Object minutes}) => '${minutes} Minuten'; - @override String get rememberTrackSelections => 'Spurauswahl pro Serie/Film merken'; - @override String get rememberTrackSelectionsDescription => 'Audio- und Untertitelsprache automatisch speichern, wenn während der Wiedergabe geändert'; - @override String get unwatchedOnly => 'Nur ungesehene'; - @override String get unwatchedOnlyDescription => 'Nur ungesehene Episoden in die Shuffle-Warteschlange aufnehmen'; - @override String get shuffleOrderNavigation => 'Navigation der Shuffle-Reihenfolge'; - @override String get shuffleOrderNavigationDescription => 'Weiter/Zurück folgt der zufälligen Reihenfolge'; - @override String get loopShuffleQueue => 'Shuffle-Warteschlange wiederholen'; - @override String get loopShuffleQueueDescription => 'Warteschlange neu starten, wenn das Ende erreicht ist'; - @override String get videoPlayerControls => 'Videoplayer-Steuerung'; - @override String get keyboardShortcuts => 'Tastenkürzel'; - @override String get keyboardShortcutsDescription => 'Tastenkürzel anpassen'; - @override String get debugLogging => 'Debug-Protokollierung'; - @override String get debugLoggingDescription => 'Detaillierte Protokolle zur Fehleranalyse aktivieren'; - @override String get viewLogs => 'Protokolle anzeigen'; - @override String get viewLogsDescription => 'App-Protokolle anzeigen'; - @override String get clearCache => 'Cache löschen'; - @override String get clearCacheDescription => 'Löscht alle zwischengespeicherten Bilder und Daten. Die App kann danach langsamer laden.'; - @override String get clearCacheSuccess => 'Cache erfolgreich gelöscht'; - @override String get resetSettings => 'Einstellungen zurücksetzen'; - @override String get resetSettingsDescription => 'Alle Einstellungen auf Standard zurücksetzen. Dies kann nicht rückgängig gemacht werden.'; - @override String get resetSettingsSuccess => 'Einstellungen erfolgreich zurückgesetzt'; - @override String get shortcutsReset => 'Tastenkürzel auf Standard zurückgesetzt'; - @override String get about => 'Über'; - @override String get aboutDescription => 'App-Informationen und Lizenzen'; - @override String get updates => 'Updates'; - @override String get updateAvailable => 'Update verfügbar'; - @override String get checkForUpdates => 'Nach Updates suchen'; - @override String get validationErrorEnterNumber => 'Bitte eine gültige Zahl eingeben'; - @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 autoSkip => 'Automatisches Überspringen'; - @override String get autoSkipIntro => 'Intro automatisch überspringen'; - @override String get autoSkipIntroDescription => 'Intro-Marker nach wenigen Sekunden automatisch überspringen'; - @override String get autoSkipCredits => 'Abspann automatisch überspringen'; - @override String get autoSkipCreditsDescription => 'Abspann automatisch überspringen und nächste Episode abspielen'; - @override String get autoSkipDelay => 'Verzögerung für automatisches Überspringen'; - @override String autoSkipDelayDescription({required Object seconds}) => '${seconds} Sekunden vor dem automatischen Überspringen warten'; + // Translations + @override + String get title => 'Einstellungen'; + @override + String get language => 'Sprache'; + @override + String get theme => 'Design'; + @override + String get appearance => 'Darstellung'; + @override + String get videoPlayback => 'Videowiedergabe'; + @override + String get shufflePlay => 'Zufallswiedergabe'; + @override + String get advanced => 'Erweitert'; + @override + String get useSeasonPostersDescription => + 'Staffelposter statt Serienposter für Episoden anzeigen'; + @override + String get showHeroSectionDescription => + 'Bereich mit empfohlenen Inhalten auf der Startseite anzeigen'; + @override + String get secondsLabel => 'Sekunden'; + @override + String get minutesLabel => 'Minuten'; + @override + String get secondsShort => 's'; + @override + String get minutesShort => 'm'; + @override + String durationHint({required Object min, required Object max}) => + 'Dauer eingeben (${min}-${max})'; + @override + String get systemTheme => 'System'; + @override + String get systemThemeDescription => 'Systemeinstellungen folgen'; + @override + String get lightTheme => 'Hell'; + @override + String get darkTheme => 'Dunkel'; + @override + String get libraryDensity => 'Mediathekdichte'; + @override + String get compact => 'Kompakt'; + @override + String get compactDescription => 'Kleinere Karten, mehr Elemente sichtbar'; + @override + String get normal => 'Normal'; + @override + String get normalDescription => 'Standardgröße'; + @override + String get comfortable => 'Großzügig'; + @override + String get comfortableDescription => + 'Größere Karten, weniger Elemente sichtbar'; + @override + String get viewMode => 'Ansichtsmodus'; + @override + String get gridView => 'Raster'; + @override + String get gridViewDescription => 'Elemente im Raster anzeigen'; + @override + String get listView => 'Liste'; + @override + String get listViewDescription => 'Elemente in Listenansicht anzeigen'; + @override + String get useSeasonPosters => 'Staffelposter verwenden'; + @override + String get showHeroSection => 'Hero-Bereich anzeigen'; + @override + String get hardwareDecoding => 'Hardware-Decodierung'; + @override + String get hardwareDecodingDescription => + 'Hardwarebeschleunigung verwenden, sofern verfügbar'; + @override + String get bufferSize => 'Puffergröße'; + @override + String bufferSizeMB({required Object size}) => '${size}MB'; + @override + String get subtitleStyling => 'Untertitel-Stil'; + @override + String get subtitleStylingDescription => 'Aussehen von Untertiteln anpassen'; + @override + String get smallSkipDuration => 'Kleine Sprungdauer'; + @override + String get largeSkipDuration => 'Große Sprungdauer'; + @override + String secondsUnit({required Object seconds}) => '${seconds} Sekunden'; + @override + String get defaultSleepTimer => 'Standard-Sleep-Timer'; + @override + String minutesUnit({required Object minutes}) => '${minutes} Minuten'; + @override + String get rememberTrackSelections => 'Spurauswahl pro Serie/Film merken'; + @override + String get rememberTrackSelectionsDescription => + 'Audio- und Untertitelsprache automatisch speichern, wenn während der Wiedergabe geändert'; + @override + String get unwatchedOnly => 'Nur ungesehene'; + @override + String get unwatchedOnlyDescription => + 'Nur ungesehene Episoden in die Shuffle-Warteschlange aufnehmen'; + @override + String get shuffleOrderNavigation => 'Navigation der Shuffle-Reihenfolge'; + @override + String get shuffleOrderNavigationDescription => + 'Weiter/Zurück folgt der zufälligen Reihenfolge'; + @override + String get loopShuffleQueue => 'Shuffle-Warteschlange wiederholen'; + @override + String get loopShuffleQueueDescription => + 'Warteschlange neu starten, wenn das Ende erreicht ist'; + @override + String get videoPlayerControls => 'Videoplayer-Steuerung'; + @override + String get keyboardShortcuts => 'Tastenkürzel'; + @override + String get keyboardShortcutsDescription => 'Tastenkürzel anpassen'; + @override + String get debugLogging => 'Debug-Protokollierung'; + @override + String get debugLoggingDescription => + 'Detaillierte Protokolle zur Fehleranalyse aktivieren'; + @override + String get viewLogs => 'Protokolle anzeigen'; + @override + String get viewLogsDescription => 'App-Protokolle anzeigen'; + @override + String get clearCache => 'Cache löschen'; + @override + String get clearCacheDescription => + 'Löscht alle zwischengespeicherten Bilder und Daten. Die App kann danach langsamer laden.'; + @override + String get clearCacheSuccess => 'Cache erfolgreich gelöscht'; + @override + String get resetSettings => 'Einstellungen zurücksetzen'; + @override + String get resetSettingsDescription => + 'Alle Einstellungen auf Standard zurücksetzen. Dies kann nicht rückgängig gemacht werden.'; + @override + String get resetSettingsSuccess => 'Einstellungen erfolgreich zurückgesetzt'; + @override + String get shortcutsReset => 'Tastenkürzel auf Standard zurückgesetzt'; + @override + String get about => 'Über'; + @override + String get aboutDescription => 'App-Informationen und Lizenzen'; + @override + String get updates => 'Updates'; + @override + String get updateAvailable => 'Update verfügbar'; + @override + String get checkForUpdates => 'Nach Updates suchen'; + @override + String get validationErrorEnterNumber => 'Bitte eine gültige Zahl eingeben'; + @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 autoSkip => 'Automatisches Überspringen'; + @override + String get autoSkipIntro => 'Intro automatisch überspringen'; + @override + String get autoSkipIntroDescription => + 'Intro-Marker nach wenigen Sekunden automatisch überspringen'; + @override + String get autoSkipCredits => 'Abspann automatisch überspringen'; + @override + String get autoSkipCreditsDescription => + 'Abspann automatisch überspringen und nächste Episode abspielen'; + @override + String get autoSkipDelay => 'Verzögerung für automatisches Überspringen'; + @override + String autoSkipDelayDescription({required Object seconds}) => + '${seconds} Sekunden vor dem automatischen Überspringen warten'; + @override + String get downloads => 'Downloads'; + @override + String get downloadLocationDescription => + 'Speicherort für heruntergeladene Inhalte wählen'; + @override + String get downloadLocationDefault => 'Standard (App-Speicher)'; + @override + String get downloadLocationCustom => 'Benutzerdefinierter Speicherort'; + @override + String get selectFolder => 'Ordner auswählen'; + @override + String get resetToDefault => 'Auf Standard zurücksetzen'; + @override + String currentPath({required Object path}) => 'Aktuell: ${path}'; + @override + String get downloadLocationNotSupported => + 'Benutzerdefinierter Download-Speicherort wird auf iOS nicht unterstützt'; + @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 downloadOnWifiOnly => 'Nur über WLAN herunterladen'; + @override + String get downloadOnWifiOnlyDescription => + 'Downloads über mobile Daten verhindern'; + @override + String get cellularDownloadBlocked => + 'Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.'; } // Path: search class _StringsSearchDe implements _StringsSearchEn { - _StringsSearchDe._(this._root); + _StringsSearchDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get hint => 'Filme, Serien, Musik suchen...'; - @override String get tryDifferentTerm => 'Anderen Suchbegriff versuchen'; - @override String get searchYourMedia => 'In den eigenen Medien suchen'; - @override String get enterTitleActorOrKeyword => 'Titel, Schauspieler oder Stichwort eingeben'; + // Translations + @override + String get hint => 'Filme, Serien, Musik suchen...'; + @override + String get tryDifferentTerm => 'Anderen Suchbegriff versuchen'; + @override + String get searchYourMedia => 'In den eigenen Medien suchen'; + @override + String get enterTitleActorOrKeyword => + 'Titel, Schauspieler oder Stichwort eingeben'; } // Path: hotkeys class _StringsHotkeysDe implements _StringsHotkeysEn { - _StringsHotkeysDe._(this._root); + _StringsHotkeysDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String setShortcutFor({required Object actionName}) => 'Tastenkürzel festlegen für ${actionName}'; - @override String get clearShortcut => 'Kürzel löschen'; + // Translations + @override + String setShortcutFor({required Object actionName}) => + 'Tastenkürzel festlegen für ${actionName}'; + @override + String get clearShortcut => 'Kürzel löschen'; } // Path: pinEntry class _StringsPinEntryDe implements _StringsPinEntryEn { - _StringsPinEntryDe._(this._root); + _StringsPinEntryDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get enterPin => 'PIN eingeben'; - @override String get showPin => 'PIN anzeigen'; - @override String get hidePin => 'PIN verbergen'; + // Translations + @override + String get enterPin => 'PIN eingeben'; + @override + String get showPin => 'PIN anzeigen'; + @override + String get hidePin => 'PIN verbergen'; } // Path: fileInfo class _StringsFileInfoDe implements _StringsFileInfoEn { - _StringsFileInfoDe._(this._root); + _StringsFileInfoDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Dateiinfo'; - @override String get video => 'Video'; - @override String get audio => 'Audio'; - @override String get file => 'Datei'; - @override String get advanced => 'Erweitert'; - @override String get codec => 'Codec'; - @override String get resolution => 'Auflösung'; - @override String get bitrate => 'Bitrate'; - @override String get frameRate => 'Bildrate'; - @override String get aspectRatio => 'Seitenverhältnis'; - @override String get profile => 'Profil'; - @override String get bitDepth => 'Farbtiefe'; - @override String get colorSpace => 'Farbraum'; - @override String get colorRange => 'Farbbereich'; - @override String get colorPrimaries => 'Primärfarben'; - @override String get chromaSubsampling => 'Chroma-Subsampling'; - @override String get channels => 'Kanäle'; - @override String get path => 'Pfad'; - @override String get size => 'Größe'; - @override String get container => 'Container'; - @override String get duration => 'Dauer'; - @override String get optimizedForStreaming => 'Für Streaming optimiert'; - @override String get has64bitOffsets => '64-Bit-Offsets'; + // Translations + @override + String get title => 'Dateiinfo'; + @override + String get video => 'Video'; + @override + String get audio => 'Audio'; + @override + String get file => 'Datei'; + @override + String get advanced => 'Erweitert'; + @override + String get codec => 'Codec'; + @override + String get resolution => 'Auflösung'; + @override + String get bitrate => 'Bitrate'; + @override + String get frameRate => 'Bildrate'; + @override + String get aspectRatio => 'Seitenverhältnis'; + @override + String get profile => 'Profil'; + @override + String get bitDepth => 'Farbtiefe'; + @override + String get colorSpace => 'Farbraum'; + @override + String get colorRange => 'Farbbereich'; + @override + String get colorPrimaries => 'Primärfarben'; + @override + String get chromaSubsampling => 'Chroma-Subsampling'; + @override + String get channels => 'Kanäle'; + @override + String get path => 'Pfad'; + @override + String get size => 'Größe'; + @override + String get container => 'Container'; + @override + String get duration => 'Dauer'; + @override + String get optimizedForStreaming => 'Für Streaming optimiert'; + @override + String get has64bitOffsets => '64-Bit-Offsets'; } // Path: mediaMenu class _StringsMediaMenuDe implements _StringsMediaMenuEn { - _StringsMediaMenuDe._(this._root); + _StringsMediaMenuDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get markAsWatched => 'Als gesehen markieren'; - @override String get markAsUnwatched => 'Als ungesehen markieren'; - @override String get removeFromContinueWatching => 'Aus ‚Weiterschauen‘ entfernen'; - @override String get goToSeries => 'Zur Serie'; - @override String get goToSeason => 'Zur Staffel'; - @override String get shufflePlay => 'Zufallswiedergabe'; - @override String get fileInfo => 'Dateiinfo'; + // Translations + @override + String get markAsWatched => 'Als gesehen markieren'; + @override + String get markAsUnwatched => 'Als ungesehen markieren'; + @override + String get removeFromContinueWatching => 'Aus ‚Weiterschauen‘ entfernen'; + @override + String get goToSeries => 'Zur Serie'; + @override + String get goToSeason => 'Zur Staffel'; + @override + String get shufflePlay => 'Zufallswiedergabe'; + @override + String get fileInfo => 'Dateiinfo'; } // Path: accessibility class _StringsAccessibilityDe implements _StringsAccessibilityEn { - _StringsAccessibilityDe._(this._root); + _StringsAccessibilityDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String mediaCardMovie({required Object title}) => '${title}, Film'; - @override String mediaCardShow({required Object title}) => '${title}, Serie'; - @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - @override String get mediaCardWatched => 'angesehen'; - @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} Prozent angesehen'; - @override String get mediaCardUnwatched => 'ungeschaut'; - @override String get tapToPlay => 'Zum Abspielen tippen'; + // Translations + @override + String mediaCardMovie({required Object title}) => '${title}, Film'; + @override + String mediaCardShow({required Object title}) => '${title}, Serie'; + @override + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + @override + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + @override + String get mediaCardWatched => 'angesehen'; + @override + String mediaCardPartiallyWatched({required Object percent}) => + '${percent} Prozent angesehen'; + @override + String get mediaCardUnwatched => 'ungeschaut'; + @override + String get tapToPlay => 'Zum Abspielen tippen'; } // Path: tooltips class _StringsTooltipsDe implements _StringsTooltipsEn { - _StringsTooltipsDe._(this._root); + _StringsTooltipsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get shufflePlay => 'Zufallswiedergabe'; - @override String get markAsWatched => 'Als gesehen markieren'; - @override String get markAsUnwatched => 'Als ungesehen markieren'; + // Translations + @override + String get shufflePlay => 'Zufallswiedergabe'; + @override + String get markAsWatched => 'Als gesehen markieren'; + @override + String get markAsUnwatched => 'Als ungesehen markieren'; } // Path: videoControls class _StringsVideoControlsDe implements _StringsVideoControlsEn { - _StringsVideoControlsDe._(this._root); + _StringsVideoControlsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get audioLabel => 'Audio'; - @override String get subtitlesLabel => 'Untertitel'; - @override String get resetToZero => 'Auf 0 ms zurücksetzen'; - @override String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - @override String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - @override String playsLater({required Object label}) => '${label} spielt später'; - @override String playsEarlier({required Object label}) => '${label} spielt früher'; - @override String get noOffset => 'Kein Offset'; - @override String get letterbox => 'Letterbox'; - @override String get fillScreen => 'Bild füllen'; - @override String get stretch => 'Strecken'; - @override String get lockRotation => 'Rotation sperren'; - @override String get unlockRotation => 'Rotation entsperren'; - @override String get sleepTimer => 'Schlaftimer'; - @override String get timerActive => 'Schlaftimer aktiv'; - @override String playbackWillPauseIn({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert'; - @override String get sleepTimerCompleted => 'Schlaftimer abgelaufen – Wiedergabe pausiert'; - @override String get playButton => 'Wiedergeben'; - @override String get pauseButton => 'Pause'; - @override String seekBackwardButton({required Object seconds}) => '${seconds} Sekunden zurück'; - @override String seekForwardButton({required Object seconds}) => '${seconds} Sekunden vor'; - @override String get previousButton => 'Vorherige Episode'; - @override String get nextButton => 'Nächste Episode'; - @override String get previousChapterButton => 'Vorheriges Kapitel'; - @override String get nextChapterButton => 'Nächstes Kapitel'; - @override String get volumeButton => 'Lautstärke'; - @override String get muteButton => 'Stumm schalten'; - @override String get unmuteButton => 'Stummschaltung aufheben'; - @override String get settingsButton => 'Videoeinstellungen'; - @override String get audioTrackButton => 'Tonspuren'; - @override String get subtitlesButton => 'Untertitel'; - @override String get chaptersButton => 'Kapitel'; - @override String get versionsButton => 'Videoversionen'; - @override String get aspectRatioButton => 'Seitenverhältnis'; - @override String get fullscreenButton => 'Vollbild aktivieren'; - @override String get exitFullscreenButton => 'Vollbild verlassen'; - @override String get rotationLockButton => 'Dreh­sperre'; - @override String get timelineSlider => 'Video-Zeitleiste'; - @override String get volumeSlider => 'Lautstärkepegel'; - @override String get backButton => 'Zurück'; + // Translations + @override + String get audioLabel => 'Audio'; + @override + String get subtitlesLabel => 'Untertitel'; + @override + String get resetToZero => 'Auf 0 ms zurücksetzen'; + @override + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + @override + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + @override + String playsLater({required Object label}) => '${label} spielt später'; + @override + String playsEarlier({required Object label}) => '${label} spielt früher'; + @override + String get noOffset => 'Kein Offset'; + @override + String get letterbox => 'Letterbox'; + @override + String get fillScreen => 'Bild füllen'; + @override + String get stretch => 'Strecken'; + @override + String get lockRotation => 'Rotation sperren'; + @override + String get unlockRotation => 'Rotation entsperren'; + @override + String get sleepTimer => 'Schlaftimer'; + @override + String get timerActive => 'Schlaftimer aktiv'; + @override + String playbackWillPauseIn({required Object duration}) => + 'Wiedergabe wird in ${duration} pausiert'; + @override + String get sleepTimerCompleted => + 'Schlaftimer abgelaufen – Wiedergabe pausiert'; + @override + String get playButton => 'Wiedergeben'; + @override + String get pauseButton => 'Pause'; + @override + String seekBackwardButton({required Object seconds}) => + '${seconds} Sekunden zurück'; + @override + String seekForwardButton({required Object seconds}) => + '${seconds} Sekunden vor'; + @override + String get previousButton => 'Vorherige Episode'; + @override + String get nextButton => 'Nächste Episode'; + @override + String get previousChapterButton => 'Vorheriges Kapitel'; + @override + String get nextChapterButton => 'Nächstes Kapitel'; + @override + String get volumeButton => 'Lautstärke'; + @override + String get muteButton => 'Stumm schalten'; + @override + String get unmuteButton => 'Stummschaltung aufheben'; + @override + String get settingsButton => 'Videoeinstellungen'; + @override + String get audioTrackButton => 'Tonspuren'; + @override + String get subtitlesButton => 'Untertitel'; + @override + String get chaptersButton => 'Kapitel'; + @override + String get versionsButton => 'Videoversionen'; + @override + String get aspectRatioButton => 'Seitenverhältnis'; + @override + String get fullscreenButton => 'Vollbild aktivieren'; + @override + String get exitFullscreenButton => 'Vollbild verlassen'; + @override + String get rotationLockButton => 'Dreh­sperre'; + @override + String get timelineSlider => 'Video-Zeitleiste'; + @override + String get volumeSlider => 'Lautstärkepegel'; + @override + String get backButton => 'Zurück'; } // Path: userStatus class _StringsUserStatusDe implements _StringsUserStatusEn { - _StringsUserStatusDe._(this._root); + _StringsUserStatusDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get admin => 'Eigentümer'; - @override String get restricted => 'Eingeschränkt'; - @override String get protected => 'Geschützt'; - @override String get current => 'AKTUELL'; + // Translations + @override + String get admin => 'Eigentümer'; + @override + String get restricted => 'Eingeschränkt'; + @override + String get protected => 'Geschützt'; + @override + String get current => 'AKTUELL'; } // Path: messages class _StringsMessagesDe implements _StringsMessagesEn { - _StringsMessagesDe._(this._root); + _StringsMessagesDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get markedAsWatched => 'Als gesehen markiert'; - @override String get markedAsUnwatched => 'Als ungesehen markiert'; - @override String get removedFromContinueWatching => 'Aus ‚Weiterschauen‘ entfernt'; - @override String errorLoading({required Object error}) => 'Fehler: ${error}'; - @override String get fileInfoNotAvailable => 'Dateiinfo nicht verfügbar'; - @override String errorLoadingFileInfo({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}'; - @override String get errorLoadingSeries => 'Fehler beim Laden der Serie'; - @override String get errorLoadingSeason => 'Fehler beim Laden der Staffel'; - @override String get musicNotSupported => 'Musikwiedergabe wird noch nicht unterstützt'; - @override String get logsCleared => 'Protokolle gelöscht'; - @override String get logsCopied => 'Protokolle in Zwischenablage kopiert'; - @override String get noLogsAvailable => 'Keine Protokolle verfügbar'; - @override String libraryScanning({required Object title}) => 'Scanne „${title}“...'; - @override String libraryScanStarted({required Object title}) => 'Mediathekscan gestartet für „${title}“'; - @override String libraryScanFailed({required Object error}) => 'Fehler beim Scannen der Mediathek: ${error}'; - @override String metadataRefreshing({required Object title}) => 'Metadaten werden aktualisiert für „${title}“...'; - @override String metadataRefreshStarted({required Object title}) => 'Metadaten-Aktualisierung gestartet für „${title}“'; - @override String metadataRefreshFailed({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}'; - @override String get noPlexToken => 'Kein Plex-Token gefunden. Bitte erneut anmelden.'; - @override String get logoutConfirm => 'Abmeldung wirklich durchführen?'; - @override String get noSeasonsFound => 'Keine Staffeln gefunden'; - @override String get noEpisodesFound => 'Keine Episoden in der ersten Staffel gefunden'; - @override String get noEpisodesFoundGeneral => 'Keine Episoden gefunden'; - @override String get noResultsFound => 'Keine Ergebnisse gefunden'; - @override String sleepTimerSet({required Object label}) => 'Sleep-Timer gesetzt auf ${label}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Profilwechsel zu ${displayName} fehlgeschlagen'; - @override String get noItemsAvailable => 'Keine Elemente verfügbar'; - @override String get failedToCreatePlayQueue => 'Wiedergabewarteschlange konnte nicht erstellt werden'; - @override String get failedToCreatePlayQueueNoItems => 'Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente'; - @override String failedPlayback({required Object action, required Object error}) => 'Wiedergabe für ${action} fehlgeschlagen: ${error}'; + // Translations + @override + String get markedAsWatched => 'Als gesehen markiert'; + @override + String get markedAsUnwatched => 'Als ungesehen markiert'; + @override + String get markedAsWatchedOffline => + 'Als gesehen markiert (wird synchronisiert, wenn online)'; + @override + String get markedAsUnwatchedOffline => + 'Als ungesehen markiert (wird synchronisiert, wenn online)'; + @override + String get removedFromContinueWatching => 'Aus ‚Weiterschauen\' entfernt'; + @override + String errorLoading({required Object error}) => 'Fehler: ${error}'; + @override + String get fileInfoNotAvailable => 'Dateiinfo nicht verfügbar'; + @override + String errorLoadingFileInfo({required Object error}) => + 'Fehler beim Laden der Dateiinfo: ${error}'; + @override + String get errorLoadingSeries => 'Fehler beim Laden der Serie'; + @override + String get errorLoadingSeason => 'Fehler beim Laden der Staffel'; + @override + String get musicNotSupported => 'Musikwiedergabe wird noch nicht unterstützt'; + @override + String get logsCleared => 'Protokolle gelöscht'; + @override + String get logsCopied => 'Protokolle in Zwischenablage kopiert'; + @override + String get noLogsAvailable => 'Keine Protokolle verfügbar'; + @override + String libraryScanning({required Object title}) => 'Scanne „${title}“...'; + @override + String libraryScanStarted({required Object title}) => + 'Mediathekscan gestartet für „${title}“'; + @override + String libraryScanFailed({required Object error}) => + 'Fehler beim Scannen der Mediathek: ${error}'; + @override + String metadataRefreshing({required Object title}) => + 'Metadaten werden aktualisiert für „${title}“...'; + @override + String metadataRefreshStarted({required Object title}) => + 'Metadaten-Aktualisierung gestartet für „${title}“'; + @override + String metadataRefreshFailed({required Object error}) => + 'Metadaten konnten nicht aktualisiert werden: ${error}'; + @override + String get noPlexToken => 'Kein Plex-Token gefunden. Bitte erneut anmelden.'; + @override + String get logoutConfirm => 'Abmeldung wirklich durchführen?'; + @override + String get noSeasonsFound => 'Keine Staffeln gefunden'; + @override + String get noEpisodesFound => 'Keine Episoden in der ersten Staffel gefunden'; + @override + String get noEpisodesFoundGeneral => 'Keine Episoden gefunden'; + @override + String get noResultsFound => 'Keine Ergebnisse gefunden'; + @override + String sleepTimerSet({required Object label}) => + 'Sleep-Timer gesetzt auf ${label}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Profilwechsel zu ${displayName} fehlgeschlagen'; + @override + String get noItemsAvailable => 'Keine Elemente verfügbar'; + @override + String get failedToCreatePlayQueue => + 'Wiedergabewarteschlange konnte nicht erstellt werden'; + @override + String get failedToCreatePlayQueueNoItems => + 'Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente'; + @override + String failedPlayback({required Object action, required Object error}) => + 'Wiedergabe für ${action} fehlgeschlagen: ${error}'; + @override + String get youAreOffline => 'Du bist offline'; + @override + String get offlineFeatureUnavailable => + 'Diese Funktion erfordert eine Internetverbindung. Deine Downloads und Einstellungen sind weiterhin verfügbar.'; } // Path: profile class _StringsProfileDe implements _StringsProfileEn { - _StringsProfileDe._(this._root); + _StringsProfileDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get noUsersAvailable => 'Keine Benutzer verfügbar'; + // Translations + @override + String get noUsersAvailable => 'Keine Benutzer verfügbar'; } // Path: subtitlingStyling class _StringsSubtitlingStylingDe implements _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingDe._(this._root); + _StringsSubtitlingStylingDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get stylingOptions => 'Stiloptionen'; - @override String get fontSize => 'Schriftgröße'; - @override String get textColor => 'Textfarbe'; - @override String get borderSize => 'Rahmengröße'; - @override String get borderColor => 'Rahmenfarbe'; - @override String get backgroundOpacity => 'Hintergrunddeckkraft'; - @override String get backgroundColor => 'Hintergrundfarbe'; + // Translations + @override + String get stylingOptions => 'Stiloptionen'; + @override + String get fontSize => 'Schriftgröße'; + @override + String get textColor => 'Textfarbe'; + @override + String get borderSize => 'Rahmengröße'; + @override + String get borderColor => 'Rahmenfarbe'; + @override + String get backgroundOpacity => 'Hintergrunddeckkraft'; + @override + String get backgroundColor => 'Hintergrundfarbe'; } // Path: dialog class _StringsDialogDe implements _StringsDialogEn { - _StringsDialogDe._(this._root); + _StringsDialogDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get confirmAction => 'Aktion bestätigen'; - @override String get areYouSure => 'Aktion wirklich ausführen?'; - @override String get cancel => 'Abbrechen'; - @override String get playNow => 'Jetzt abspielen'; + // Translations + @override + String get confirmAction => 'Aktion bestätigen'; + @override + String get areYouSure => 'Aktion wirklich ausführen?'; + @override + String get cancel => 'Abbrechen'; + @override + String get playNow => 'Jetzt abspielen'; } // Path: discover class _StringsDiscoverDe implements _StringsDiscoverEn { - _StringsDiscoverDe._(this._root); + _StringsDiscoverDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Entdecken'; - @override String get switchProfile => 'Profil wechseln'; - @override String get switchServer => 'Server wechseln'; - @override String get logout => 'Abmelden'; - @override String get noContentAvailable => 'Kein Inhalt verfügbar'; - @override String get addMediaToLibraries => 'Medien zur Mediathek hinzufügen'; - @override String get continueWatching => 'Weiterschauen'; - @override String get recentlyAdded => 'Kürzlich hinzugefügt'; - @override String get play => 'Abspielen'; - @override String get resume => 'Fortsetzen'; - @override String playEpisode({required Object season, required Object episode}) => 'S${season}, E${episode} - Abspielen'; - @override String resumeEpisode({required Object season, required Object episode}) => 'S${season}, E${episode} - Fortsetzen'; - @override String get pause => 'Pause'; - @override String get overview => 'Übersicht'; - @override String get cast => 'Besetzung'; - @override String get seasons => 'Staffeln'; - @override String get studio => 'Studio'; - @override String get rating => 'Altersfreigabe'; - @override String get watched => 'Gesehen'; - @override String episodeCount({required Object count}) => '${count} Episoden'; - @override String watchedProgress({required Object watched, required Object total}) => '${watched} von ${total} gesehen'; - @override String get movie => 'Film'; - @override String get tvShow => 'Serie'; - @override String minutesLeft({required Object minutes}) => '${minutes} Min übrig'; + // Translations + @override + String get title => 'Entdecken'; + @override + String get switchProfile => 'Profil wechseln'; + @override + String get switchServer => 'Server wechseln'; + @override + String get logout => 'Abmelden'; + @override + String get noContentAvailable => 'Kein Inhalt verfügbar'; + @override + String get addMediaToLibraries => 'Medien zur Mediathek hinzufügen'; + @override + String get continueWatching => 'Weiterschauen'; + @override + String get recentlyAdded => 'Kürzlich hinzugefügt'; + @override + String get play => 'Abspielen'; + @override + String get resume => 'Fortsetzen'; + @override + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String get pause => 'Pause'; + @override + String get overview => 'Übersicht'; + @override + String get cast => 'Besetzung'; + @override + String get seasons => 'Staffeln'; + @override + String get studio => 'Studio'; + @override + String get rating => 'Altersfreigabe'; + @override + String get watched => 'Gesehen'; + @override + String episodeCount({required Object count}) => '${count} Episoden'; + @override + String watchedProgress({required Object watched, required Object total}) => + '${watched} von ${total} gesehen'; + @override + String get movie => 'Film'; + @override + String get tvShow => 'Serie'; + @override + String minutesLeft({required Object minutes}) => '${minutes} Min übrig'; } // Path: errors class _StringsErrorsDe implements _StringsErrorsEn { - _StringsErrorsDe._(this._root); + _StringsErrorsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String searchFailed({required Object error}) => 'Suche fehlgeschlagen: ${error}'; - @override String connectionTimeout({required Object context}) => 'Zeitüberschreitung beim Laden von ${context}'; - @override String get connectionFailed => 'Verbindung zum Plex-Server fehlgeschlagen'; - @override String failedToLoad({required Object context, required Object error}) => 'Fehler beim Laden von ${context}: ${error}'; - @override String get noClientAvailable => 'Kein Client verfügbar'; - @override String authenticationFailed({required Object error}) => 'Authentifizierung fehlgeschlagen: ${error}'; - @override String get couldNotLaunchUrl => 'Auth-URL konnte nicht geöffnet werden'; - @override String get pleaseEnterToken => 'Bitte Token eingeben'; - @override String get invalidToken => 'Ungültiges Token'; - @override String failedToVerifyToken({required Object error}) => 'Token-Verifizierung fehlgeschlagen: ${error}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Profilwechsel zu ${displayName} fehlgeschlagen'; - @override String get connectionFailedGeneric => 'Verbindung fehlgeschlagen'; + // Translations + @override + String searchFailed({required Object error}) => + 'Suche fehlgeschlagen: ${error}'; + @override + String connectionTimeout({required Object context}) => + 'Zeitüberschreitung beim Laden von ${context}'; + @override + String get connectionFailed => 'Verbindung zum Plex-Server fehlgeschlagen'; + @override + String failedToLoad({required Object context, required Object error}) => + 'Fehler beim Laden von ${context}: ${error}'; + @override + String get noClientAvailable => 'Kein Client verfügbar'; + @override + String authenticationFailed({required Object error}) => + 'Authentifizierung fehlgeschlagen: ${error}'; + @override + String get couldNotLaunchUrl => 'Auth-URL konnte nicht geöffnet werden'; + @override + String get pleaseEnterToken => 'Bitte Token eingeben'; + @override + String get invalidToken => 'Ungültiges Token'; + @override + String failedToVerifyToken({required Object error}) => + 'Token-Verifizierung fehlgeschlagen: ${error}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Profilwechsel zu ${displayName} fehlgeschlagen'; + @override + String get connectionFailedGeneric => 'Verbindung fehlgeschlagen'; } // Path: libraries class _StringsLibrariesDe implements _StringsLibrariesEn { - _StringsLibrariesDe._(this._root); + _StringsLibrariesDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Mediatheken'; - @override String get scanLibraryFiles => 'Mediatheksdateien scannen'; - @override String get scanLibrary => 'Mediathek scannen'; - @override String get analyze => 'Analysieren'; - @override String get analyzeLibrary => 'Mediathek analysieren'; - @override String get refreshMetadata => 'Metadaten aktualisieren'; - @override String get emptyTrash => 'Papierkorb leeren'; - @override String emptyingTrash({required Object title}) => 'Papierkorb für „${title}“ wird geleert...'; - @override String trashEmptied({required Object title}) => 'Papierkorb für „${title}“ geleert'; - @override String failedToEmptyTrash({required Object error}) => 'Papierkorb konnte nicht geleert werden: ${error}'; - @override String analyzing({required Object title}) => 'Analysiere „${title}“...'; - @override String analysisStarted({required Object title}) => 'Analyse gestartet für „${title}“'; - @override String failedToAnalyze({required Object error}) => 'Analyse der Mediathek fehlgeschlagen: ${error}'; - @override String get noLibrariesFound => 'Keine Mediatheken gefunden'; - @override String get thisLibraryIsEmpty => 'Diese Mediathek ist leer'; - @override String get all => 'Alle'; - @override String get clearAll => 'Alle löschen'; - @override String scanLibraryConfirm({required Object title}) => '„${title}“ wirklich scannen?'; - @override String analyzeLibraryConfirm({required Object title}) => '„${title}“ wirklich analysieren?'; - @override String refreshMetadataConfirm({required Object title}) => 'Metadaten für „${title}“ wirklich aktualisieren?'; - @override String emptyTrashConfirm({required Object title}) => 'Papierkorb für „${title}“ wirklich leeren?'; - @override String get manageLibraries => 'Mediatheken verwalten'; - @override String get sort => 'Sortieren'; - @override String get sortBy => 'Sortieren nach'; - @override String get filters => 'Filter'; - @override String loadingLibraryWithCount({required Object count}) => 'Lade Mediathek... (${count} Elemente geladen)'; - @override String get confirmActionMessage => 'Aktion wirklich durchführen?'; - @override String get showLibrary => 'Mediathek anzeigen'; - @override String get hideLibrary => 'Mediathek ausblenden'; - @override String get libraryOptions => 'Mediatheksoptionen'; - @override String get content => 'Bibliotheksinhalt'; - @override String get selectLibrary => 'Bibliothek auswählen'; - @override String filtersWithCount({required Object count}) => 'Filter (${count})'; - @override String get noRecommendations => 'Keine Empfehlungen verfügbar'; - @override String get noCollections => 'Keine Sammlungen in dieser Mediathek'; - @override String get noFoldersFound => 'Keine Ordner gefunden'; - @override String get folders => 'Ordner'; - @override late final _StringsLibrariesTabsDe tabs = _StringsLibrariesTabsDe._(_root); - @override late final _StringsLibrariesGroupingsDe groupings = _StringsLibrariesGroupingsDe._(_root); + // Translations + @override + String get title => 'Mediatheken'; + @override + String get scanLibraryFiles => 'Mediatheksdateien scannen'; + @override + String get scanLibrary => 'Mediathek scannen'; + @override + String get analyze => 'Analysieren'; + @override + String get analyzeLibrary => 'Mediathek analysieren'; + @override + String get refreshMetadata => 'Metadaten aktualisieren'; + @override + String get emptyTrash => 'Papierkorb leeren'; + @override + String emptyingTrash({required Object title}) => + 'Papierkorb für „${title}“ wird geleert...'; + @override + String trashEmptied({required Object title}) => + 'Papierkorb für „${title}“ geleert'; + @override + String failedToEmptyTrash({required Object error}) => + 'Papierkorb konnte nicht geleert werden: ${error}'; + @override + String analyzing({required Object title}) => 'Analysiere „${title}“...'; + @override + String analysisStarted({required Object title}) => + 'Analyse gestartet für „${title}“'; + @override + String failedToAnalyze({required Object error}) => + 'Analyse der Mediathek fehlgeschlagen: ${error}'; + @override + String get noLibrariesFound => 'Keine Mediatheken gefunden'; + @override + String get thisLibraryIsEmpty => 'Diese Mediathek ist leer'; + @override + String get all => 'Alle'; + @override + String get clearAll => 'Alle löschen'; + @override + String scanLibraryConfirm({required Object title}) => + '„${title}“ wirklich scannen?'; + @override + String analyzeLibraryConfirm({required Object title}) => + '„${title}“ wirklich analysieren?'; + @override + String refreshMetadataConfirm({required Object title}) => + 'Metadaten für „${title}“ wirklich aktualisieren?'; + @override + String emptyTrashConfirm({required Object title}) => + 'Papierkorb für „${title}“ wirklich leeren?'; + @override + String get manageLibraries => 'Mediatheken verwalten'; + @override + String get sort => 'Sortieren'; + @override + String get sortBy => 'Sortieren nach'; + @override + String get filters => 'Filter'; + @override + String loadingLibraryWithCount({required Object count}) => + 'Lade Mediathek... (${count} Elemente geladen)'; + @override + String get confirmActionMessage => 'Aktion wirklich durchführen?'; + @override + String get showLibrary => 'Mediathek anzeigen'; + @override + String get hideLibrary => 'Mediathek ausblenden'; + @override + String get libraryOptions => 'Mediatheksoptionen'; + @override + String get content => 'Bibliotheksinhalt'; + @override + String get selectLibrary => 'Bibliothek auswählen'; + @override + String filtersWithCount({required Object count}) => 'Filter (${count})'; + @override + String get noRecommendations => 'Keine Empfehlungen verfügbar'; + @override + String get noCollections => 'Keine Sammlungen in dieser Mediathek'; + @override + String get noFoldersFound => 'Keine Ordner gefunden'; + @override + String get folders => 'Ordner'; + @override + late final _StringsLibrariesTabsDe tabs = _StringsLibrariesTabsDe._(_root); + @override + late final _StringsLibrariesGroupingsDe groupings = + _StringsLibrariesGroupingsDe._(_root); } // Path: about class _StringsAboutDe implements _StringsAboutEn { - _StringsAboutDe._(this._root); + _StringsAboutDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Über'; - @override String get openSourceLicenses => 'Open-Source-Lizenzen'; - @override String versionLabel({required Object version}) => 'Version ${version}'; - @override String get appDescription => 'Ein schöner Plex-Client für Flutter'; - @override String get viewLicensesDescription => 'Lizenzen von Drittanbieter-Bibliotheken anzeigen'; + // Translations + @override + String get title => 'Über'; + @override + String get openSourceLicenses => 'Open-Source-Lizenzen'; + @override + String versionLabel({required Object version}) => 'Version ${version}'; + @override + String get appDescription => 'Ein schöner Plex-Client für Flutter'; + @override + String get viewLicensesDescription => + 'Lizenzen von Drittanbieter-Bibliotheken anzeigen'; } // Path: serverSelection class _StringsServerSelectionDe implements _StringsServerSelectionEn { - _StringsServerSelectionDe._(this._root); + _StringsServerSelectionDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get connectingToServer => 'Verbinde mit Server...'; - @override String get connectingToAllServers => 'Verbinde mit allen Servern...'; - @override String get connectToAllServers => 'Mit allen Servern verbinden'; - @override String get allServerConnectionsFailed => 'Verbindung zu allen Servern fehlgeschlagen. Bitte Netzwerk prüfen und erneut versuchen.'; - @override String foundServers({required Object count}) => '${count} Server gefunden'; - @override String get orSelectIndividually => 'Oder einzelnen Server auswählen:'; - @override String get serverDebugCopied => 'Server-Debugdaten kopiert'; - @override String get copyDebugData => 'Debugdaten kopieren'; - @override String get noServersFound => 'Keine Server gefunden'; - @override String noServersFoundForAccount({required Object username, required Object email}) => 'Keine Server gefunden für ${username} (${email})'; - @override String malformedServerData({required Object count}) => '${count} Server mit fehlerhaften Daten gefunden. Keine gültigen Server verfügbar.'; - @override String get incompleteServerInfo => 'Einige Serverinformationen sind unvollständig und wurden übersprungen. Plex.tv-Konto prüfen.'; - @override String get incompleteConnectionInfo => 'Serververbindungsinformationen unvollständig. Bitte erneut versuchen.'; - @override String malformedServerInfo({required Object message}) => 'Serverinformationen fehlerhaft: ${message}'; - @override String get networkConnectionFailed => 'Netzwerkverbindung fehlgeschlagen. Internetverbindung prüfen und erneut versuchen.'; - @override String get authenticationFailed => 'Authentifizierung fehlgeschlagen. Bitte erneut anmelden.'; - @override String get plexServiceUnavailable => 'Plex-Dienst nicht verfügbar. Bitte später erneut versuchen.'; - @override String failedToLoadServers({required Object error}) => 'Server konnten nicht geladen werden: ${error}'; + // Translations + @override + String get connectingToServer => 'Verbinde mit Server...'; + @override + String get connectingToAllServers => 'Verbinde mit allen Servern...'; + @override + String get connectToAllServers => 'Mit allen Servern verbinden'; + @override + String get allServerConnectionsFailed => + 'Verbindung zu allen Servern fehlgeschlagen. Bitte Netzwerk prüfen und erneut versuchen.'; + @override + String foundServers({required Object count}) => '${count} Server gefunden'; + @override + String get orSelectIndividually => 'Oder einzelnen Server auswählen:'; + @override + String get serverDebugCopied => 'Server-Debugdaten kopiert'; + @override + String get copyDebugData => 'Debugdaten kopieren'; + @override + String get noServersFound => 'Keine Server gefunden'; + @override + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => 'Keine Server gefunden für ${username} (${email})'; + @override + String malformedServerData({required Object count}) => + '${count} Server mit fehlerhaften Daten gefunden. Keine gültigen Server verfügbar.'; + @override + String get incompleteServerInfo => + 'Einige Serverinformationen sind unvollständig und wurden übersprungen. Plex.tv-Konto prüfen.'; + @override + String get incompleteConnectionInfo => + 'Serververbindungsinformationen unvollständig. Bitte erneut versuchen.'; + @override + String malformedServerInfo({required Object message}) => + 'Serverinformationen fehlerhaft: ${message}'; + @override + String get networkConnectionFailed => + 'Netzwerkverbindung fehlgeschlagen. Internetverbindung prüfen und erneut versuchen.'; + @override + String get authenticationFailed => + 'Authentifizierung fehlgeschlagen. Bitte erneut anmelden.'; + @override + String get plexServiceUnavailable => + 'Plex-Dienst nicht verfügbar. Bitte später erneut versuchen.'; + @override + String failedToLoadServers({required Object error}) => + 'Server konnten nicht geladen werden: ${error}'; } // Path: hubDetail class _StringsHubDetailDe implements _StringsHubDetailEn { - _StringsHubDetailDe._(this._root); + _StringsHubDetailDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Titel'; - @override String get releaseYear => 'Erscheinungsjahr'; - @override String get dateAdded => 'Hinzugefügt am'; - @override String get rating => 'Bewertung'; - @override String get noItemsFound => 'Keine Elemente gefunden'; + // Translations + @override + String get title => 'Titel'; + @override + String get releaseYear => 'Erscheinungsjahr'; + @override + String get dateAdded => 'Hinzugefügt am'; + @override + String get rating => 'Bewertung'; + @override + String get noItemsFound => 'Keine Elemente gefunden'; } // Path: logs class _StringsLogsDe implements _StringsLogsEn { - _StringsLogsDe._(this._root); + _StringsLogsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Protokolle'; - @override String get clearLogs => 'Protokolle löschen'; - @override String get copyLogs => 'Protokolle kopieren'; - @override String get exportLogs => 'Protokolle exportieren'; - @override String get noLogsToShow => 'Keine Protokolle zum Anzeigen'; - @override String get error => 'Fehler:'; - @override String get stackTrace => 'Stacktrace:'; + // Translations + @override + String get title => 'Protokolle'; + @override + String get clearLogs => 'Protokolle löschen'; + @override + String get copyLogs => 'Protokolle kopieren'; + @override + String get exportLogs => 'Protokolle exportieren'; + @override + String get noLogsToShow => 'Keine Protokolle zum Anzeigen'; + @override + String get error => 'Fehler:'; + @override + String get stackTrace => 'Stacktrace:'; } // Path: licenses class _StringsLicensesDe implements _StringsLicensesEn { - _StringsLicensesDe._(this._root); + _StringsLicensesDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get relatedPackages => 'Verwandte Pakete'; - @override String get license => 'Lizenz'; - @override String licenseNumber({required Object number}) => 'Lizenz ${number}'; - @override String licensesCount({required Object count}) => '${count} Lizenzen'; + // Translations + @override + String get relatedPackages => 'Verwandte Pakete'; + @override + String get license => 'Lizenz'; + @override + String licenseNumber({required Object number}) => 'Lizenz ${number}'; + @override + String licensesCount({required Object count}) => '${count} Lizenzen'; } // Path: navigation class _StringsNavigationDe implements _StringsNavigationEn { - _StringsNavigationDe._(this._root); + _StringsNavigationDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get home => 'Start'; - @override String get search => 'Suche'; - @override String get libraries => 'Mediatheken'; - @override String get settings => 'Einstellungen'; + // Translations + @override + String get home => 'Start'; + @override + String get search => 'Suche'; + @override + String get libraries => 'Mediatheken'; + @override + String get settings => 'Einstellungen'; + @override + String get downloads => 'Downloads'; +} + +// Path: downloads +class _StringsDownloadsDe implements _StringsDownloadsEn { + _StringsDownloadsDe._(this._root); + + @override + final _StringsDe _root; // ignore: unused_field + + // Translations + @override + String get title => 'Downloads'; + @override + String get manage => 'Verwalten'; + @override + String get tvShows => 'Serien'; + @override + String get movies => 'Filme'; + @override + String get queue => 'Warteschlange'; + @override + String get noDownloads => 'Noch keine Downloads'; + @override + String get noDownloadsDescription => + 'Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt'; + @override + String get browseContent => 'Inhalte durchsuchen'; + @override + String get downloading => 'Wird heruntergeladen'; + @override + String get downloaded => 'Heruntergeladen'; + @override + String get paused => 'Pausiert'; + @override + String get failed => 'Fehlgeschlagen'; + @override + String get queued => 'In Warteschlange'; + @override + String get downloadNow => 'Herunterladen'; + @override + String get deleteDownload => 'Download löschen'; + @override + String get cancelDownload => 'Download abbrechen'; + @override + String get pauseDownload => 'Download pausieren'; + @override + String get resumeDownload => 'Download fortsetzen'; + @override + String get retryDownload => 'Download wiederholen'; + @override + String get downloadComplete => 'Download abgeschlossen'; + @override + String get downloadFailed => 'Download fehlgeschlagen'; + @override + String get downloadCancelled => 'Download abgebrochen'; + @override + String get downloadQueued => 'Download in Warteschlange'; + @override + String episodesQueued({required Object count}) => + '${count} Episoden zum Download hinzugefügt'; + @override + String get downloadDeleted => 'Download gelöscht'; + @override + String get deleteConfirmTitle => 'Download löschen?'; + @override + String deleteConfirm({required Object title}) => + 'Möchtest du "${title}" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.'; + @override + String get deleteConfirmMessage => + 'Die heruntergeladene Datei wird von deinem Gerät entfernt.'; + @override + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => 'Lösche ${title}... (${current} von ${total})'; + @override + String deletingSimple({required Object title}) => 'Lösche ${title}...'; + @override + String get storageUsed => 'Speicherplatz belegt'; + @override + String get availableOffline => 'Offline verfügbar'; + @override + String get downloadingSeason => 'Staffel wird heruntergeladen'; + @override + String get downloadingEpisode => 'Episode wird heruntergeladen'; +} + +// Path: offline +class _StringsOfflineDe implements _StringsOfflineEn { + _StringsOfflineDe._(this._root); + + @override + final _StringsDe _root; // ignore: unused_field + + // Translations + @override + String get youAreOffline => 'Du bist offline'; + @override + String get offlineMode => 'Offline-Modus'; + @override + String get backOnline => 'Wieder online'; + @override + String get showingDownloadedContent => + 'Nur heruntergeladene Inhalte werden angezeigt'; + @override + String get noInternetConnection => 'Keine Internetverbindung'; + @override + String get serversUnreachable => 'Plex-Server nicht erreichbar'; + @override + String get connectToDownload => + 'Verbinde dich mit dem Internet, um Inhalte herunterzuladen'; } // Path: playlists class _StringsPlaylistsDe implements _StringsPlaylistsEn { - _StringsPlaylistsDe._(this._root); + _StringsPlaylistsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Wiedergabelisten'; - @override String get noPlaylists => 'Keine Wiedergabelisten gefunden'; - @override String get create => 'Wiedergabeliste erstellen'; - @override String get newPlaylist => 'Neue Wiedergabeliste'; - @override String get playlistName => 'Name der Wiedergabeliste'; - @override String get enterPlaylistName => 'Name der Wiedergabeliste eingeben'; - @override String get edit => 'Wiedergabeliste bearbeiten'; - @override String get delete => 'Wiedergabeliste löschen'; - @override String get addTo => 'Zur Wiedergabeliste hinzufügen'; - @override String get addItems => 'Elemente hinzufügen'; - @override String get removeItem => 'Aus Wiedergabeliste entfernen'; - @override String get clearPlaylist => 'Wiedergabeliste leeren'; - @override String get playAll => 'Alle abspielen'; - @override String get shuffle => 'Zufällig'; - @override String get smartPlaylist => 'Intelligente Wiedergabeliste'; - @override String get regularPlaylist => 'Normale Wiedergabeliste'; - @override String itemCount({required Object count}) => '${count} Elemente'; - @override String get oneItem => '1 Element'; - @override String get emptyPlaylist => 'Diese Wiedergabeliste ist leer'; - @override String get deleteConfirm => 'Wiedergabeliste löschen?'; - @override String deleteMessage({required Object name}) => 'Soll "${name}" wirklich gelöscht werden?'; - @override String get created => 'Wiedergabeliste erstellt'; - @override String get updated => 'Wiedergabeliste aktualisiert'; - @override String get deleted => 'Wiedergabeliste gelöscht'; - @override String get itemAdded => 'Zur Wiedergabeliste hinzugefügt'; - @override String get itemRemoved => 'Aus Wiedergabeliste entfernt'; - @override String get selectPlaylist => 'Wiedergabeliste auswählen'; - @override String get createNewPlaylist => 'Neue Wiedergabeliste erstellen'; - @override String get errorCreating => 'Wiedergabeliste konnte nicht erstellt werden'; - @override String get errorDeleting => 'Wiedergabeliste konnte nicht gelöscht werden'; - @override String get errorLoading => 'Wiedergabelisten konnten nicht geladen werden'; - @override String get errorAdding => 'Konnte nicht zur Wiedergabeliste hinzugefügt werden'; - @override String get errorReordering => 'Element der Wiedergabeliste konnte nicht neu geordnet werden'; - @override String get errorRemoving => 'Konnte nicht aus der Wiedergabeliste entfernt werden'; - @override String get playlist => 'Wiedergabeliste'; + // Translations + @override + String get title => 'Wiedergabelisten'; + @override + String get noPlaylists => 'Keine Wiedergabelisten gefunden'; + @override + String get create => 'Wiedergabeliste erstellen'; + @override + String get newPlaylist => 'Neue Wiedergabeliste'; + @override + String get playlistName => 'Name der Wiedergabeliste'; + @override + String get enterPlaylistName => 'Name der Wiedergabeliste eingeben'; + @override + String get edit => 'Wiedergabeliste bearbeiten'; + @override + String get delete => 'Wiedergabeliste löschen'; + @override + String get addTo => 'Zur Wiedergabeliste hinzufügen'; + @override + String get addItems => 'Elemente hinzufügen'; + @override + String get removeItem => 'Aus Wiedergabeliste entfernen'; + @override + String get clearPlaylist => 'Wiedergabeliste leeren'; + @override + String get playAll => 'Alle abspielen'; + @override + String get shuffle => 'Zufällig'; + @override + String get smartPlaylist => 'Intelligente Wiedergabeliste'; + @override + String get regularPlaylist => 'Normale Wiedergabeliste'; + @override + String itemCount({required Object count}) => '${count} Elemente'; + @override + String get oneItem => '1 Element'; + @override + String get emptyPlaylist => 'Diese Wiedergabeliste ist leer'; + @override + String get deleteConfirm => 'Wiedergabeliste löschen?'; + @override + String deleteMessage({required Object name}) => + 'Soll "${name}" wirklich gelöscht werden?'; + @override + String get created => 'Wiedergabeliste erstellt'; + @override + String get updated => 'Wiedergabeliste aktualisiert'; + @override + String get deleted => 'Wiedergabeliste gelöscht'; + @override + String get itemAdded => 'Zur Wiedergabeliste hinzugefügt'; + @override + String get itemRemoved => 'Aus Wiedergabeliste entfernt'; + @override + String get selectPlaylist => 'Wiedergabeliste auswählen'; + @override + String get createNewPlaylist => 'Neue Wiedergabeliste erstellen'; + @override + String get errorCreating => 'Wiedergabeliste konnte nicht erstellt werden'; + @override + String get errorDeleting => 'Wiedergabeliste konnte nicht gelöscht werden'; + @override + String get errorLoading => 'Wiedergabelisten konnten nicht geladen werden'; + @override + String get errorAdding => + 'Konnte nicht zur Wiedergabeliste hinzugefügt werden'; + @override + String get errorReordering => + 'Element der Wiedergabeliste konnte nicht neu geordnet werden'; + @override + String get errorRemoving => + 'Konnte nicht aus der Wiedergabeliste entfernt werden'; + @override + String get playlist => 'Wiedergabeliste'; } // Path: collections class _StringsCollectionsDe implements _StringsCollectionsEn { - _StringsCollectionsDe._(this._root); + _StringsCollectionsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get title => 'Sammlungen'; - @override String get collection => 'Sammlung'; - @override String get empty => 'Sammlung ist leer'; - @override String get noItems => 'Keine Elemente in dieser Sammlung'; - @override String get unknownLibrarySection => 'Löschen nicht möglich: Unbekannte Bibliothekssektion'; - @override String get deleteCollection => 'Sammlung löschen'; - @override String deleteConfirm({required Object title}) => 'Sind Sie sicher, dass Sie "${title}" löschen möchten? Dies kann nicht rückgängig gemacht werden.'; - @override String get deleted => 'Sammlung gelöscht'; - @override String get deleteFailed => 'Sammlung konnte nicht gelöscht werden'; - @override String deleteFailedWithError({required Object error}) => 'Sammlung konnte nicht gelöscht werden: ${error}'; - @override String failedToLoadItems({required Object error}) => 'Sammlungselemente konnten nicht geladen werden: ${error}'; - @override String get addTo => 'Zur Sammlung hinzufügen'; - @override String get selectCollection => 'Sammlung auswählen'; - @override String get createNewCollection => 'Neue Sammlung erstellen'; - @override String get collectionName => 'Sammlungsname'; - @override String get enterCollectionName => 'Sammlungsnamen eingeben'; - @override String get addedToCollection => 'Zur Sammlung hinzugefügt'; - @override String get errorAddingToCollection => 'Fehler beim Hinzufügen zur Sammlung'; - @override String get created => 'Sammlung erstellt'; - @override String get removeFromCollection => 'Aus Sammlung entfernen'; - @override String removeFromCollectionConfirm({required Object title}) => '"${title}" aus dieser Sammlung entfernen?'; - @override String get removedFromCollection => 'Aus Sammlung entfernt'; - @override String get removeFromCollectionFailed => 'Entfernen aus Sammlung fehlgeschlagen'; - @override String removeFromCollectionError({required Object error}) => 'Fehler beim Entfernen aus der Sammlung: ${error}'; + // Translations + @override + String get title => 'Sammlungen'; + @override + String get collection => 'Sammlung'; + @override + String get empty => 'Sammlung ist leer'; + @override + String get noItems => 'Keine Elemente in dieser Sammlung'; + @override + String get unknownLibrarySection => + 'Löschen nicht möglich: Unbekannte Bibliothekssektion'; + @override + String get deleteCollection => 'Sammlung löschen'; + @override + String deleteConfirm({required Object title}) => + 'Sind Sie sicher, dass Sie "${title}" löschen möchten? Dies kann nicht rückgängig gemacht werden.'; + @override + String get deleted => 'Sammlung gelöscht'; + @override + String get deleteFailed => 'Sammlung konnte nicht gelöscht werden'; + @override + String deleteFailedWithError({required Object error}) => + 'Sammlung konnte nicht gelöscht werden: ${error}'; + @override + String failedToLoadItems({required Object error}) => + 'Sammlungselemente konnten nicht geladen werden: ${error}'; + @override + String get addTo => 'Zur Sammlung hinzufügen'; + @override + String get selectCollection => 'Sammlung auswählen'; + @override + String get createNewCollection => 'Neue Sammlung erstellen'; + @override + String get collectionName => 'Sammlungsname'; + @override + String get enterCollectionName => 'Sammlungsnamen eingeben'; + @override + String get addedToCollection => 'Zur Sammlung hinzugefügt'; + @override + String get errorAddingToCollection => 'Fehler beim Hinzufügen zur Sammlung'; + @override + String get created => 'Sammlung erstellt'; + @override + String get removeFromCollection => 'Aus Sammlung entfernen'; + @override + String removeFromCollectionConfirm({required Object title}) => + '"${title}" aus dieser Sammlung entfernen?'; + @override + String get removedFromCollection => 'Aus Sammlung entfernt'; + @override + String get removeFromCollectionFailed => + 'Entfernen aus Sammlung fehlgeschlagen'; + @override + String removeFromCollectionError({required Object error}) => + 'Fehler beim Entfernen aus der Sammlung: ${error}'; } // Path: libraries.tabs class _StringsLibrariesTabsDe implements _StringsLibrariesTabsEn { - _StringsLibrariesTabsDe._(this._root); + _StringsLibrariesTabsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get recommended => 'Empfohlen'; - @override String get browse => 'Durchsuchen'; - @override String get collections => 'Sammlungen'; - @override String get playlists => 'Wiedergabelisten'; + // Translations + @override + String get recommended => 'Empfohlen'; + @override + String get browse => 'Durchsuchen'; + @override + String get collections => 'Sammlungen'; + @override + String get playlists => 'Wiedergabelisten'; } // Path: libraries.groupings class _StringsLibrariesGroupingsDe implements _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsDe._(this._root); + _StringsLibrariesGroupingsDe._(this._root); - @override final _StringsDe _root; // ignore: unused_field + @override + final _StringsDe _root; // ignore: unused_field - // Translations - @override String get all => 'Alle'; - @override String get movies => 'Filme'; - @override String get shows => 'Serien'; - @override String get seasons => 'Staffeln'; - @override String get episodes => 'Episoden'; - @override String get folders => 'Ordner'; + // Translations + @override + String get all => 'Alle'; + @override + String get movies => 'Filme'; + @override + String get shows => 'Serien'; + @override + String get seasons => 'Staffeln'; + @override + String get episodes => 'Episoden'; + @override + String get folders => 'Ordner'; } // Path: class _StringsIt implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsIt.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.it, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsIt.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.it, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); - @override late final _StringsIt _root = this; // ignore: unused_field + @override + late final _StringsIt _root = this; // ignore: unused_field - // Translations - @override late final _StringsAppIt app = _StringsAppIt._(_root); - @override late final _StringsAuthIt auth = _StringsAuthIt._(_root); - @override late final _StringsCommonIt common = _StringsCommonIt._(_root); - @override late final _StringsScreensIt screens = _StringsScreensIt._(_root); - @override late final _StringsUpdateIt update = _StringsUpdateIt._(_root); - @override late final _StringsSettingsIt settings = _StringsSettingsIt._(_root); - @override late final _StringsSearchIt search = _StringsSearchIt._(_root); - @override late final _StringsHotkeysIt hotkeys = _StringsHotkeysIt._(_root); - @override late final _StringsPinEntryIt pinEntry = _StringsPinEntryIt._(_root); - @override late final _StringsFileInfoIt fileInfo = _StringsFileInfoIt._(_root); - @override late final _StringsMediaMenuIt mediaMenu = _StringsMediaMenuIt._(_root); - @override late final _StringsAccessibilityIt accessibility = _StringsAccessibilityIt._(_root); - @override late final _StringsTooltipsIt tooltips = _StringsTooltipsIt._(_root); - @override late final _StringsVideoControlsIt videoControls = _StringsVideoControlsIt._(_root); - @override late final _StringsUserStatusIt userStatus = _StringsUserStatusIt._(_root); - @override late final _StringsMessagesIt messages = _StringsMessagesIt._(_root); - @override late final _StringsProfileIt profile = _StringsProfileIt._(_root); - @override late final _StringsSubtitlingStylingIt subtitlingStyling = _StringsSubtitlingStylingIt._(_root); - @override late final _StringsDialogIt dialog = _StringsDialogIt._(_root); - @override late final _StringsDiscoverIt discover = _StringsDiscoverIt._(_root); - @override late final _StringsErrorsIt errors = _StringsErrorsIt._(_root); - @override late final _StringsLibrariesIt libraries = _StringsLibrariesIt._(_root); - @override late final _StringsAboutIt about = _StringsAboutIt._(_root); - @override late final _StringsServerSelectionIt serverSelection = _StringsServerSelectionIt._(_root); - @override late final _StringsHubDetailIt hubDetail = _StringsHubDetailIt._(_root); - @override late final _StringsLogsIt logs = _StringsLogsIt._(_root); - @override late final _StringsLicensesIt licenses = _StringsLicensesIt._(_root); - @override late final _StringsNavigationIt navigation = _StringsNavigationIt._(_root); - @override late final _StringsPlaylistsIt playlists = _StringsPlaylistsIt._(_root); - @override late final _StringsCollectionsIt collections = _StringsCollectionsIt._(_root); + // Translations + @override + late final _StringsAppIt app = _StringsAppIt._(_root); + @override + late final _StringsAuthIt auth = _StringsAuthIt._(_root); + @override + late final _StringsCommonIt common = _StringsCommonIt._(_root); + @override + late final _StringsScreensIt screens = _StringsScreensIt._(_root); + @override + late final _StringsUpdateIt update = _StringsUpdateIt._(_root); + @override + late final _StringsSettingsIt settings = _StringsSettingsIt._(_root); + @override + late final _StringsSearchIt search = _StringsSearchIt._(_root); + @override + late final _StringsHotkeysIt hotkeys = _StringsHotkeysIt._(_root); + @override + late final _StringsPinEntryIt pinEntry = _StringsPinEntryIt._(_root); + @override + late final _StringsFileInfoIt fileInfo = _StringsFileInfoIt._(_root); + @override + late final _StringsMediaMenuIt mediaMenu = _StringsMediaMenuIt._(_root); + @override + late final _StringsAccessibilityIt accessibility = _StringsAccessibilityIt._( + _root, + ); + @override + late final _StringsTooltipsIt tooltips = _StringsTooltipsIt._(_root); + @override + late final _StringsVideoControlsIt videoControls = _StringsVideoControlsIt._( + _root, + ); + @override + late final _StringsUserStatusIt userStatus = _StringsUserStatusIt._(_root); + @override + late final _StringsMessagesIt messages = _StringsMessagesIt._(_root); + @override + late final _StringsProfileIt profile = _StringsProfileIt._(_root); + @override + late final _StringsSubtitlingStylingIt subtitlingStyling = + _StringsSubtitlingStylingIt._(_root); + @override + late final _StringsDialogIt dialog = _StringsDialogIt._(_root); + @override + late final _StringsDiscoverIt discover = _StringsDiscoverIt._(_root); + @override + late final _StringsErrorsIt errors = _StringsErrorsIt._(_root); + @override + late final _StringsLibrariesIt libraries = _StringsLibrariesIt._(_root); + @override + late final _StringsAboutIt about = _StringsAboutIt._(_root); + @override + late final _StringsServerSelectionIt serverSelection = + _StringsServerSelectionIt._(_root); + @override + late final _StringsHubDetailIt hubDetail = _StringsHubDetailIt._(_root); + @override + late final _StringsLogsIt logs = _StringsLogsIt._(_root); + @override + late final _StringsLicensesIt licenses = _StringsLicensesIt._(_root); + @override + late final _StringsNavigationIt navigation = _StringsNavigationIt._(_root); + @override + late final _StringsDownloadsIt downloads = _StringsDownloadsIt._(_root); + @override + late final _StringsOfflineIt offline = _StringsOfflineIt._(_root); + @override + late final _StringsPlaylistsIt playlists = _StringsPlaylistsIt._(_root); + @override + late final _StringsCollectionsIt collections = _StringsCollectionsIt._(_root); } // Path: app class _StringsAppIt implements _StringsAppEn { - _StringsAppIt._(this._root); + _StringsAppIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Plezy'; - @override String get loading => 'Caricamento...'; + // Translations + @override + String get title => 'Plezy'; + @override + String get loading => 'Caricamento...'; } // Path: auth class _StringsAuthIt implements _StringsAuthEn { - _StringsAuthIt._(this._root); + _StringsAuthIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get signInWithPlex => 'Accedi con Plex'; - @override String get showQRCode => 'Mostra QR Code'; - @override String get cancel => 'Cancella'; - @override String get authenticate => 'Autenticazione'; - @override String get retry => 'Riprova'; - @override String get debugEnterToken => 'Debug: Inserisci Token Plex'; - @override String get plexTokenLabel => 'Token Auth Plex'; - @override String get plexTokenHint => 'Inserisci il tuo token di Plex.tv'; - @override String get authenticationTimeout => 'Autenticazione scaduta. Riprova.'; - @override String get scanQRCodeInstruction => 'Scansiona questo QR code con un dispositivo connesso a Plex per autenticarti.'; - @override String get waitingForAuth => 'In attesa di autenticazione...\nCompleta l\'accesso dal tuo browser.'; + // Translations + @override + String get signInWithPlex => 'Accedi con Plex'; + @override + String get showQRCode => 'Mostra QR Code'; + @override + String get cancel => 'Cancella'; + @override + String get authenticate => 'Autenticazione'; + @override + String get retry => 'Riprova'; + @override + String get debugEnterToken => 'Debug: Inserisci Token Plex'; + @override + String get plexTokenLabel => 'Token Auth Plex'; + @override + String get plexTokenHint => 'Inserisci il tuo token di Plex.tv'; + @override + String get authenticationTimeout => 'Autenticazione scaduta. Riprova.'; + @override + String get scanQRCodeInstruction => + 'Scansiona questo QR code con un dispositivo connesso a Plex per autenticarti.'; + @override + String get waitingForAuth => + 'In attesa di autenticazione...\nCompleta l\'accesso dal tuo browser.'; } // Path: common class _StringsCommonIt implements _StringsCommonEn { - _StringsCommonIt._(this._root); + _StringsCommonIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get cancel => 'Cancella'; - @override String get save => 'Salva'; - @override String get close => 'Chiudi'; - @override String get clear => 'Pulisci'; - @override String get reset => 'Ripristina'; - @override String get later => 'Più tardi'; - @override String get submit => 'Invia'; - @override String get confirm => 'Conferma'; - @override String get retry => 'Riprova'; - @override String get playNow => 'Riproduci ora'; - @override String get logout => 'Disconnetti'; - @override String get online => 'Online'; - @override String get offline => 'Offline'; - @override String get owned => 'Di proprietà'; - @override String get shared => 'Condiviso'; - @override String get current => 'CORRENTE'; - @override String get unknown => 'Sconosciuto'; - @override String get refresh => 'Aggiorna'; - @override String get yes => 'Sì'; - @override String get no => 'No'; - @override String get server => 'Server'; - @override String get delete => 'Elimina'; - @override String get shuffle => 'Casuale'; - @override String get addTo => 'Aggiungi a...'; + // Translations + @override + String get cancel => 'Cancella'; + @override + String get save => 'Salva'; + @override + String get close => 'Chiudi'; + @override + String get clear => 'Pulisci'; + @override + String get reset => 'Ripristina'; + @override + String get later => 'Più tardi'; + @override + String get submit => 'Invia'; + @override + String get confirm => 'Conferma'; + @override + String get retry => 'Riprova'; + @override + String get playNow => 'Riproduci ora'; + @override + String get logout => 'Disconnetti'; + @override + String get online => 'Online'; + @override + String get offline => 'Offline'; + @override + String get owned => 'Di proprietà'; + @override + String get shared => 'Condiviso'; + @override + String get current => 'CORRENTE'; + @override + String get unknown => 'Sconosciuto'; + @override + String get refresh => 'Aggiorna'; + @override + String get yes => 'Sì'; + @override + String get no => 'No'; + @override + String get server => 'Server'; + @override + String get delete => 'Elimina'; + @override + String get shuffle => 'Casuale'; + @override + String get addTo => 'Aggiungi a...'; } // Path: screens class _StringsScreensIt implements _StringsScreensEn { - _StringsScreensIt._(this._root); + _StringsScreensIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get licenses => 'Licenze'; - @override String get selectServer => 'Seleziona server'; - @override String get switchProfile => 'Cambia profilo'; - @override String get subtitleStyling => 'Stile sottotitoli'; - @override String get search => 'Cerca'; - @override String get logs => 'Registro'; + // Translations + @override + String get licenses => 'Licenze'; + @override + String get selectServer => 'Seleziona server'; + @override + String get switchProfile => 'Cambia profilo'; + @override + String get subtitleStyling => 'Stile sottotitoli'; + @override + String get search => 'Cerca'; + @override + String get logs => 'Registro'; } // Path: update class _StringsUpdateIt implements _StringsUpdateEn { - _StringsUpdateIt._(this._root); + _StringsUpdateIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get available => 'Aggiornamento disponibile'; - @override String versionAvailable({required Object version}) => 'Versione ${version} disponibile'; - @override String currentVersion({required Object version}) => 'Corrente: ${version}'; - @override String get skipVersion => 'Salta questa versione'; - @override String get viewRelease => 'Visualizza dettagli release'; - @override String get latestVersion => 'La versione installata è l\'ultima disponibile'; - @override String get checkFailed => 'Impossibile controllare gli aggiornamenti'; + // Translations + @override + String get available => 'Aggiornamento disponibile'; + @override + String versionAvailable({required Object version}) => + 'Versione ${version} disponibile'; + @override + String currentVersion({required Object version}) => 'Corrente: ${version}'; + @override + String get skipVersion => 'Salta questa versione'; + @override + String get viewRelease => 'Visualizza dettagli release'; + @override + String get latestVersion => 'La versione installata è l\'ultima disponibile'; + @override + String get checkFailed => 'Impossibile controllare gli aggiornamenti'; } // Path: settings class _StringsSettingsIt implements _StringsSettingsEn { - _StringsSettingsIt._(this._root); + _StringsSettingsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Impostazioni'; - @override String get language => 'Lingua'; - @override String get theme => 'Tema'; - @override String get appearance => 'Aspetto'; - @override String get videoPlayback => 'Riproduzione video'; - @override String get shufflePlay => 'Riproduzione casuale'; - @override String get advanced => 'Avanzate'; - @override String get useSeasonPostersDescription => 'Mostra il poster della stagione invece del poster della serie per gli episodi'; - @override String get showHeroSectionDescription => 'Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale'; - @override String get secondsLabel => 'Secondi'; - @override String get minutesLabel => 'Minuti'; - @override String get secondsShort => 's'; - @override String get minutesShort => 'm'; - @override String durationHint({required Object min, required Object max}) => 'Inserisci durata (${min}-${max})'; - @override String get systemTheme => 'Sistema'; - @override String get systemThemeDescription => 'Segui le impostazioni di sistema'; - @override String get lightTheme => 'Chiaro'; - @override String get darkTheme => 'Scuro'; - @override String get libraryDensity => 'Densità libreria'; - @override String get compact => 'Compatta'; - @override String get compactDescription => 'Schede più piccole, più elementi visibili'; - @override String get normal => 'Normale'; - @override String get normalDescription => 'Dimensione predefinita'; - @override String get comfortable => 'Comoda'; - @override String get comfortableDescription => 'Schede più grandi, meno elementi visibili'; - @override String get viewMode => 'Modalità di visualizzazione'; - @override String get gridView => 'Griglia'; - @override String get gridViewDescription => 'Visualizza gli elementi in un layout a griglia'; - @override String get listView => 'Elenco'; - @override String get listViewDescription => 'Visualizza gli elementi in un layout a elenco'; - @override String get useSeasonPosters => 'Usa poster delle stagioni'; - @override String get showHeroSection => 'Mostra sezione principale'; - @override String get hardwareDecoding => 'Decodifica Hardware'; - @override String get hardwareDecodingDescription => 'Utilizza l\'accelerazione hardware quando disponibile'; - @override String get bufferSize => 'Dimensione buffer'; - @override String bufferSizeMB({required Object size}) => '${size}MB'; - @override String get subtitleStyling => 'Stile sottotitoli'; - @override String get subtitleStylingDescription => 'Personalizza l\'aspetto dei sottotitoli'; - @override String get smallSkipDuration => 'Durata skip breve'; - @override String get largeSkipDuration => 'Durata skip lungo'; - @override String secondsUnit({required Object seconds}) => '${seconds} secondi'; - @override String get defaultSleepTimer => 'Timer spegnimento predefinito'; - @override String minutesUnit({required Object minutes}) => '${minutes} minuti'; - @override String get rememberTrackSelections => 'Ricorda selezioni tracce per serie/film'; - @override String get rememberTrackSelectionsDescription => 'Salva automaticamente le preferenze delle lingue audio e sottotitoli quando cambi tracce durante la riproduzione'; - @override String get unwatchedOnly => 'Solo non guardati'; - @override String get unwatchedOnlyDescription => 'Includi solo gli episodi non guardati nella coda di riproduzione casuale'; - @override String get shuffleOrderNavigation => 'Navigazione in ordine casuale'; - @override String get shuffleOrderNavigationDescription => 'I pulsanti Avanti/Indietro seguono l\'ordine casuale'; - @override String get loopShuffleQueue => 'Coda di riproduzione casuale in loop'; - @override String get loopShuffleQueueDescription => 'Riavvia la coda quando raggiungi la fine'; - @override String get videoPlayerControls => 'Controlli del lettore video'; - @override String get keyboardShortcuts => 'Scorciatoie da tastiera'; - @override String get keyboardShortcutsDescription => 'Personalizza le scorciatoie da tastiera'; - @override String get debugLogging => 'Log di debug'; - @override String get debugLoggingDescription => 'Abilita il logging dettagliato per la risoluzione dei problemi'; - @override String get viewLogs => 'Visualizza log'; - @override String get viewLogsDescription => 'Visualizza i log dell\'applicazione'; - @override String get clearCache => 'Svuota cache'; - @override String get clearCacheDescription => 'Questa opzione cancellerà tutte le immagini e i dati memorizzati nella cache. Dopo aver cancellato la cache, l\'app potrebbe impiegare più tempo per caricare i contenuti.'; - @override String get clearCacheSuccess => 'Cache cancellata correttamente'; - @override String get resetSettings => 'Ripristina impostazioni'; - @override String get resetSettingsDescription => 'Questa opzione ripristinerà tutte le impostazioni ai valori predefiniti. Non può essere annullata.'; - @override String get resetSettingsSuccess => 'Impostazioni ripristinate correttamente'; - @override String get shortcutsReset => 'Scorciatoie ripristinate alle impostazioni predefinite'; - @override String get about => 'Informazioni'; - @override String get aboutDescription => 'Informazioni sull\'app e le licenze'; - @override String get updates => 'Aggiornamenti'; - @override String get updateAvailable => 'Aggiornamento disponibile'; - @override String get checkForUpdates => 'Controlla aggiornamenti'; - @override String get validationErrorEnterNumber => 'Inserisci un numero valido'; - @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 autoSkip => 'Salto Automatico'; - @override String get autoSkipIntro => 'Salta Intro Automaticamente'; - @override String get autoSkipIntroDescription => 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi'; - @override String get autoSkipCredits => 'Salta Crediti Automaticamente'; - @override String get autoSkipCreditsDescription => 'Salta automaticamente i crediti e riproduci l\'episodio successivo'; - @override String get autoSkipDelay => 'Ritardo Salto Automatico'; - @override String autoSkipDelayDescription({required Object seconds}) => 'Aspetta ${seconds} secondi prima del salto automatico'; + // Translations + @override + String get title => 'Impostazioni'; + @override + String get language => 'Lingua'; + @override + String get theme => 'Tema'; + @override + String get appearance => 'Aspetto'; + @override + String get videoPlayback => 'Riproduzione video'; + @override + String get shufflePlay => 'Riproduzione casuale'; + @override + String get advanced => 'Avanzate'; + @override + String get useSeasonPostersDescription => + 'Mostra il poster della stagione invece del poster della serie per gli episodi'; + @override + String get showHeroSectionDescription => + 'Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale'; + @override + String get secondsLabel => 'Secondi'; + @override + String get minutesLabel => 'Minuti'; + @override + String get secondsShort => 's'; + @override + String get minutesShort => 'm'; + @override + String durationHint({required Object min, required Object max}) => + 'Inserisci durata (${min}-${max})'; + @override + String get systemTheme => 'Sistema'; + @override + String get systemThemeDescription => 'Segui le impostazioni di sistema'; + @override + String get lightTheme => 'Chiaro'; + @override + String get darkTheme => 'Scuro'; + @override + String get libraryDensity => 'Densità libreria'; + @override + String get compact => 'Compatta'; + @override + String get compactDescription => 'Schede più piccole, più elementi visibili'; + @override + String get normal => 'Normale'; + @override + String get normalDescription => 'Dimensione predefinita'; + @override + String get comfortable => 'Comoda'; + @override + String get comfortableDescription => + 'Schede più grandi, meno elementi visibili'; + @override + String get viewMode => 'Modalità di visualizzazione'; + @override + String get gridView => 'Griglia'; + @override + String get gridViewDescription => + 'Visualizza gli elementi in un layout a griglia'; + @override + String get listView => 'Elenco'; + @override + String get listViewDescription => + 'Visualizza gli elementi in un layout a elenco'; + @override + String get useSeasonPosters => 'Usa poster delle stagioni'; + @override + String get showHeroSection => 'Mostra sezione principale'; + @override + String get hardwareDecoding => 'Decodifica Hardware'; + @override + String get hardwareDecodingDescription => + 'Utilizza l\'accelerazione hardware quando disponibile'; + @override + String get bufferSize => 'Dimensione buffer'; + @override + String bufferSizeMB({required Object size}) => '${size}MB'; + @override + String get subtitleStyling => 'Stile sottotitoli'; + @override + String get subtitleStylingDescription => + 'Personalizza l\'aspetto dei sottotitoli'; + @override + String get smallSkipDuration => 'Durata skip breve'; + @override + String get largeSkipDuration => 'Durata skip lungo'; + @override + String secondsUnit({required Object seconds}) => '${seconds} secondi'; + @override + String get defaultSleepTimer => 'Timer spegnimento predefinito'; + @override + String minutesUnit({required Object minutes}) => '${minutes} minuti'; + @override + String get rememberTrackSelections => + 'Ricorda selezioni tracce per serie/film'; + @override + String get rememberTrackSelectionsDescription => + 'Salva automaticamente le preferenze delle lingue audio e sottotitoli quando cambi tracce durante la riproduzione'; + @override + String get unwatchedOnly => 'Solo non guardati'; + @override + String get unwatchedOnlyDescription => + 'Includi solo gli episodi non guardati nella coda di riproduzione casuale'; + @override + String get shuffleOrderNavigation => 'Navigazione in ordine casuale'; + @override + String get shuffleOrderNavigationDescription => + 'I pulsanti Avanti/Indietro seguono l\'ordine casuale'; + @override + String get loopShuffleQueue => 'Coda di riproduzione casuale in loop'; + @override + String get loopShuffleQueueDescription => + 'Riavvia la coda quando raggiungi la fine'; + @override + String get videoPlayerControls => 'Controlli del lettore video'; + @override + String get keyboardShortcuts => 'Scorciatoie da tastiera'; + @override + String get keyboardShortcutsDescription => + 'Personalizza le scorciatoie da tastiera'; + @override + String get debugLogging => 'Log di debug'; + @override + String get debugLoggingDescription => + 'Abilita il logging dettagliato per la risoluzione dei problemi'; + @override + String get viewLogs => 'Visualizza log'; + @override + String get viewLogsDescription => 'Visualizza i log dell\'applicazione'; + @override + String get clearCache => 'Svuota cache'; + @override + String get clearCacheDescription => + 'Questa opzione cancellerà tutte le immagini e i dati memorizzati nella cache. Dopo aver cancellato la cache, l\'app potrebbe impiegare più tempo per caricare i contenuti.'; + @override + String get clearCacheSuccess => 'Cache cancellata correttamente'; + @override + String get resetSettings => 'Ripristina impostazioni'; + @override + String get resetSettingsDescription => + 'Questa opzione ripristinerà tutte le impostazioni ai valori predefiniti. Non può essere annullata.'; + @override + String get resetSettingsSuccess => 'Impostazioni ripristinate correttamente'; + @override + String get shortcutsReset => + 'Scorciatoie ripristinate alle impostazioni predefinite'; + @override + String get about => 'Informazioni'; + @override + String get aboutDescription => 'Informazioni sull\'app e le licenze'; + @override + String get updates => 'Aggiornamenti'; + @override + String get updateAvailable => 'Aggiornamento disponibile'; + @override + String get checkForUpdates => 'Controlla aggiornamenti'; + @override + String get validationErrorEnterNumber => 'Inserisci un numero valido'; + @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 autoSkip => 'Salto Automatico'; + @override + String get autoSkipIntro => 'Salta Intro Automaticamente'; + @override + String get autoSkipIntroDescription => + 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi'; + @override + String get autoSkipCredits => 'Salta Crediti Automaticamente'; + @override + String get autoSkipCreditsDescription => + 'Salta automaticamente i crediti e riproduci l\'episodio successivo'; + @override + String get autoSkipDelay => 'Ritardo Salto Automatico'; + @override + String autoSkipDelayDescription({required Object seconds}) => + 'Aspetta ${seconds} secondi prima del salto automatico'; + @override + String get downloads => 'Download'; + @override + String get downloadLocationDescription => + 'Scegli dove salvare i contenuti scaricati'; + @override + String get downloadLocationDefault => 'Predefinita (Archiviazione App)'; + @override + String get downloadLocationCustom => 'Posizione Personalizzata'; + @override + String get selectFolder => 'Seleziona Cartella'; + @override + String get resetToDefault => 'Ripristina Predefinita'; + @override + String currentPath({required Object path}) => 'Corrente: ${path}'; + @override + String get downloadLocationNotSupported => + 'La posizione di download personalizzata non è supportata su iOS'; + @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 downloadOnWifiOnly => 'Scarica solo con WiFi'; + @override + String get downloadOnWifiOnlyDescription => + 'Impedisci i download quando si utilizza la rete dati cellulare'; + @override + String get cellularDownloadBlocked => + 'I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l\'impostazione.'; } // Path: search class _StringsSearchIt implements _StringsSearchEn { - _StringsSearchIt._(this._root); + _StringsSearchIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get hint => 'Cerca film. spettacoli, musica...'; - @override String get tryDifferentTerm => 'Prova altri termini di ricerca'; - @override String get searchYourMedia => 'Cerca nei tuoi media'; - @override String get enterTitleActorOrKeyword => 'Inserisci un titolo, attore o parola chiave'; + // Translations + @override + String get hint => 'Cerca film. spettacoli, musica...'; + @override + String get tryDifferentTerm => 'Prova altri termini di ricerca'; + @override + String get searchYourMedia => 'Cerca nei tuoi media'; + @override + String get enterTitleActorOrKeyword => + 'Inserisci un titolo, attore o parola chiave'; } // Path: hotkeys class _StringsHotkeysIt implements _StringsHotkeysEn { - _StringsHotkeysIt._(this._root); + _StringsHotkeysIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String setShortcutFor({required Object actionName}) => 'Imposta scorciatoia per ${actionName}'; - @override String get clearShortcut => 'Elimina scorciatoia'; + // Translations + @override + String setShortcutFor({required Object actionName}) => + 'Imposta scorciatoia per ${actionName}'; + @override + String get clearShortcut => 'Elimina scorciatoia'; } // Path: pinEntry class _StringsPinEntryIt implements _StringsPinEntryEn { - _StringsPinEntryIt._(this._root); + _StringsPinEntryIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get enterPin => 'Inserisci PIN'; - @override String get showPin => 'Mostra PIN'; - @override String get hidePin => 'Nascondi PIN'; + // Translations + @override + String get enterPin => 'Inserisci PIN'; + @override + String get showPin => 'Mostra PIN'; + @override + String get hidePin => 'Nascondi PIN'; } // Path: fileInfo class _StringsFileInfoIt implements _StringsFileInfoEn { - _StringsFileInfoIt._(this._root); + _StringsFileInfoIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Info sul file'; - @override String get video => 'Video'; - @override String get audio => 'Audio'; - @override String get file => 'File'; - @override String get advanced => 'Avanzate'; - @override String get codec => 'Codec'; - @override String get resolution => 'Risoluzione'; - @override String get bitrate => 'Bitrate'; - @override String get frameRate => 'Frame Rate'; - @override String get aspectRatio => 'Aspect Ratio'; - @override String get profile => 'Profilo'; - @override String get bitDepth => 'Profondità colore'; - @override String get colorSpace => 'Spazio colore'; - @override String get colorRange => 'Gamma colori'; - @override String get colorPrimaries => 'Colori primari'; - @override String get chromaSubsampling => 'Sottocampionamento cromatico'; - @override String get channels => 'Canali'; - @override String get path => 'Percorso'; - @override String get size => 'Dimensione'; - @override String get container => 'Contenitore'; - @override String get duration => 'Durata'; - @override String get optimizedForStreaming => 'Ottimizzato per lo streaming'; - @override String get has64bitOffsets => 'Offset a 64-bit'; + // Translations + @override + String get title => 'Info sul file'; + @override + String get video => 'Video'; + @override + String get audio => 'Audio'; + @override + String get file => 'File'; + @override + String get advanced => 'Avanzate'; + @override + String get codec => 'Codec'; + @override + String get resolution => 'Risoluzione'; + @override + String get bitrate => 'Bitrate'; + @override + String get frameRate => 'Frame Rate'; + @override + String get aspectRatio => 'Aspect Ratio'; + @override + String get profile => 'Profilo'; + @override + String get bitDepth => 'Profondità colore'; + @override + String get colorSpace => 'Spazio colore'; + @override + String get colorRange => 'Gamma colori'; + @override + String get colorPrimaries => 'Colori primari'; + @override + String get chromaSubsampling => 'Sottocampionamento cromatico'; + @override + String get channels => 'Canali'; + @override + String get path => 'Percorso'; + @override + String get size => 'Dimensione'; + @override + String get container => 'Contenitore'; + @override + String get duration => 'Durata'; + @override + String get optimizedForStreaming => 'Ottimizzato per lo streaming'; + @override + String get has64bitOffsets => 'Offset a 64-bit'; } // Path: mediaMenu class _StringsMediaMenuIt implements _StringsMediaMenuEn { - _StringsMediaMenuIt._(this._root); + _StringsMediaMenuIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get markAsWatched => 'Segna come visto'; - @override String get markAsUnwatched => 'Segna come non visto'; - @override String get removeFromContinueWatching => 'Rimuovi da Continua a guardare'; - @override String get goToSeries => 'Vai alle serie'; - @override String get goToSeason => 'Vai alla stagione'; - @override String get shufflePlay => 'Riproduzione casuale'; - @override String get fileInfo => 'Info sul file'; + // Translations + @override + String get markAsWatched => 'Segna come visto'; + @override + String get markAsUnwatched => 'Segna come non visto'; + @override + String get removeFromContinueWatching => 'Rimuovi da Continua a guardare'; + @override + String get goToSeries => 'Vai alle serie'; + @override + String get goToSeason => 'Vai alla stagione'; + @override + String get shufflePlay => 'Riproduzione casuale'; + @override + String get fileInfo => 'Info sul file'; } // Path: accessibility class _StringsAccessibilityIt implements _StringsAccessibilityEn { - _StringsAccessibilityIt._(this._root); + _StringsAccessibilityIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String mediaCardMovie({required Object title}) => '${title}, film'; - @override String mediaCardShow({required Object title}) => '${title}, serie TV'; - @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - @override String get mediaCardWatched => 'visto'; - @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percento visto'; - @override String get mediaCardUnwatched => 'non visto'; - @override String get tapToPlay => 'Tocca per riprodurre'; + // Translations + @override + String mediaCardMovie({required Object title}) => '${title}, film'; + @override + String mediaCardShow({required Object title}) => '${title}, serie TV'; + @override + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + @override + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + @override + String get mediaCardWatched => 'visto'; + @override + String mediaCardPartiallyWatched({required Object percent}) => + '${percent} percento visto'; + @override + String get mediaCardUnwatched => 'non visto'; + @override + String get tapToPlay => 'Tocca per riprodurre'; } // Path: tooltips class _StringsTooltipsIt implements _StringsTooltipsEn { - _StringsTooltipsIt._(this._root); + _StringsTooltipsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get shufflePlay => 'Riproduzione casuale'; - @override String get markAsWatched => 'Segna come visto'; - @override String get markAsUnwatched => 'Segna come non visto'; + // Translations + @override + String get shufflePlay => 'Riproduzione casuale'; + @override + String get markAsWatched => 'Segna come visto'; + @override + String get markAsUnwatched => 'Segna come non visto'; } // Path: videoControls class _StringsVideoControlsIt implements _StringsVideoControlsEn { - _StringsVideoControlsIt._(this._root); + _StringsVideoControlsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get audioLabel => 'Audio'; - @override String get subtitlesLabel => 'Sottotitoli'; - @override String get resetToZero => 'Riporta a 0ms'; - @override String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - @override String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - @override String playsLater({required Object label}) => '${label} riprodotto dopo'; - @override String playsEarlier({required Object label}) => '${label} riprodotto prima'; - @override String get noOffset => 'Nessun offset'; - @override String get letterbox => 'Letterbox'; - @override String get fillScreen => 'Riempi schermo'; - @override String get stretch => 'Allunga'; - @override String get lockRotation => 'Blocca rotazione'; - @override String get unlockRotation => 'Sblocca rotazione'; - @override String get sleepTimer => 'Timer di spegnimento'; - @override String get timerActive => 'Timer attivo'; - @override String playbackWillPauseIn({required Object duration}) => 'La riproduzione si interromperà tra ${duration}'; - @override String get sleepTimerCompleted => 'Timer di spegnimento completato - riproduzione in pausa'; - @override String get playButton => 'Riproduci'; - @override String get pauseButton => 'Pausa'; - @override String seekBackwardButton({required Object seconds}) => 'Riavvolgi di ${seconds} secondi'; - @override String seekForwardButton({required Object seconds}) => 'Avanza di ${seconds} secondi'; - @override String get previousButton => 'Episodio precedente'; - @override String get nextButton => 'Episodio successivo'; - @override String get previousChapterButton => 'Capitolo precedente'; - @override String get nextChapterButton => 'Capitolo successivo'; - @override String get volumeButton => 'Volume'; - @override String get muteButton => 'Silenzia'; - @override String get unmuteButton => 'Riattiva audio'; - @override String get settingsButton => 'Impostazioni video'; - @override String get audioTrackButton => 'Tracce audio'; - @override String get subtitlesButton => 'Sottotitoli'; - @override String get chaptersButton => 'Capitoli'; - @override String get versionsButton => 'Versioni video'; - @override String get aspectRatioButton => 'Proporzioni'; - @override String get fullscreenButton => 'Attiva schermo intero'; - @override String get exitFullscreenButton => 'Esci da schermo intero'; - @override String get rotationLockButton => 'Blocco rotazione'; - @override String get timelineSlider => 'Timeline video'; - @override String get volumeSlider => 'Livello volume'; - @override String get backButton => 'Indietro'; + // Translations + @override + String get audioLabel => 'Audio'; + @override + String get subtitlesLabel => 'Sottotitoli'; + @override + String get resetToZero => 'Riporta a 0ms'; + @override + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + @override + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + @override + String playsLater({required Object label}) => '${label} riprodotto dopo'; + @override + String playsEarlier({required Object label}) => '${label} riprodotto prima'; + @override + String get noOffset => 'Nessun offset'; + @override + String get letterbox => 'Letterbox'; + @override + String get fillScreen => 'Riempi schermo'; + @override + String get stretch => 'Allunga'; + @override + String get lockRotation => 'Blocca rotazione'; + @override + String get unlockRotation => 'Sblocca rotazione'; + @override + String get sleepTimer => 'Timer di spegnimento'; + @override + String get timerActive => 'Timer attivo'; + @override + String playbackWillPauseIn({required Object duration}) => + 'La riproduzione si interromperà tra ${duration}'; + @override + String get sleepTimerCompleted => + 'Timer di spegnimento completato - riproduzione in pausa'; + @override + String get playButton => 'Riproduci'; + @override + String get pauseButton => 'Pausa'; + @override + String seekBackwardButton({required Object seconds}) => + 'Riavvolgi di ${seconds} secondi'; + @override + String seekForwardButton({required Object seconds}) => + 'Avanza di ${seconds} secondi'; + @override + String get previousButton => 'Episodio precedente'; + @override + String get nextButton => 'Episodio successivo'; + @override + String get previousChapterButton => 'Capitolo precedente'; + @override + String get nextChapterButton => 'Capitolo successivo'; + @override + String get volumeButton => 'Volume'; + @override + String get muteButton => 'Silenzia'; + @override + String get unmuteButton => 'Riattiva audio'; + @override + String get settingsButton => 'Impostazioni video'; + @override + String get audioTrackButton => 'Tracce audio'; + @override + String get subtitlesButton => 'Sottotitoli'; + @override + String get chaptersButton => 'Capitoli'; + @override + String get versionsButton => 'Versioni video'; + @override + String get aspectRatioButton => 'Proporzioni'; + @override + String get fullscreenButton => 'Attiva schermo intero'; + @override + String get exitFullscreenButton => 'Esci da schermo intero'; + @override + String get rotationLockButton => 'Blocco rotazione'; + @override + String get timelineSlider => 'Timeline video'; + @override + String get volumeSlider => 'Livello volume'; + @override + String get backButton => 'Indietro'; } // Path: userStatus class _StringsUserStatusIt implements _StringsUserStatusEn { - _StringsUserStatusIt._(this._root); + _StringsUserStatusIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get admin => 'Admin'; - @override String get restricted => 'Limitato'; - @override String get protected => 'Protetto'; - @override String get current => 'ATTUALE'; + // Translations + @override + String get admin => 'Admin'; + @override + String get restricted => 'Limitato'; + @override + String get protected => 'Protetto'; + @override + String get current => 'ATTUALE'; } // Path: messages class _StringsMessagesIt implements _StringsMessagesEn { - _StringsMessagesIt._(this._root); + _StringsMessagesIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get markedAsWatched => 'Segna come visto'; - @override String get markedAsUnwatched => 'Segna come non visto'; - @override String get removedFromContinueWatching => 'Rimosso da Continua a guardare'; - @override String errorLoading({required Object error}) => 'Errore: ${error}'; - @override String get fileInfoNotAvailable => 'Informazioni sul file non disponibili'; - @override String errorLoadingFileInfo({required Object error}) => 'Errore caricamento informazioni sul file: ${error}'; - @override String get errorLoadingSeries => 'Errore caricamento serie'; - @override String get errorLoadingSeason => 'Errore caricamento stagione'; - @override String get musicNotSupported => 'La riproduzione musicale non è ancora supportata'; - @override String get logsCleared => 'Log eliminati'; - @override String get logsCopied => 'Log copiati negli appunti'; - @override String get noLogsAvailable => 'Nessun log disponibile'; - @override String libraryScanning({required Object title}) => 'Scansione "${title}"...'; - @override String libraryScanStarted({required Object title}) => 'Scansione libreria iniziata per "${title}"'; - @override String libraryScanFailed({required Object error}) => 'Impossibile eseguire scansione della libreria: ${error}'; - @override String metadataRefreshing({required Object title}) => 'Aggiornamento metadati per "${title}"...'; - @override String metadataRefreshStarted({required Object title}) => 'Aggiornamento metadati per "${title}"'; - @override String metadataRefreshFailed({required Object error}) => 'Errore aggiornamento metadati: ${error}'; - @override String get noPlexToken => 'Nessun token Plex trovato. Riesegui l\'accesso.'; - @override String get logoutConfirm => 'Sei sicuro di volerti disconnettere?'; - @override String get noSeasonsFound => 'Nessuna stagione trovata'; - @override String get noEpisodesFound => 'Nessun episodio trovato nella prima stagione'; - @override String get noEpisodesFoundGeneral => 'Nessun episodio trovato'; - @override String get noResultsFound => 'Nessun risultato'; - @override String sleepTimerSet({required Object label}) => 'Imposta timer spegnimento per ${label}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Impossibile passare a ${displayName}'; - @override String get noItemsAvailable => 'Nessun elemento disponibile'; - @override String get failedToCreatePlayQueue => 'Impossibile creare la coda di riproduzione'; - @override String get failedToCreatePlayQueueNoItems => 'Impossibile creare la coda di riproduzione - nessun elemento'; - @override String failedPlayback({required Object action, required Object error}) => 'Impossibile ${action}: ${error}'; + // Translations + @override + String get markedAsWatched => 'Segna come visto'; + @override + String get markedAsUnwatched => 'Segna come non visto'; + @override + String get markedAsWatchedOffline => + 'Segnato come visto (sincronizzato online)'; + @override + String get markedAsUnwatchedOffline => + 'Segnato come non visto (sincronizzato online)'; + @override + String get removedFromContinueWatching => 'Rimosso da Continua a guardare'; + @override + String errorLoading({required Object error}) => 'Errore: ${error}'; + @override + String get fileInfoNotAvailable => 'Informazioni sul file non disponibili'; + @override + String errorLoadingFileInfo({required Object error}) => + 'Errore caricamento informazioni sul file: ${error}'; + @override + String get errorLoadingSeries => 'Errore caricamento serie'; + @override + String get errorLoadingSeason => 'Errore caricamento stagione'; + @override + String get musicNotSupported => + 'La riproduzione musicale non è ancora supportata'; + @override + String get logsCleared => 'Log eliminati'; + @override + String get logsCopied => 'Log copiati negli appunti'; + @override + String get noLogsAvailable => 'Nessun log disponibile'; + @override + String libraryScanning({required Object title}) => 'Scansione "${title}"...'; + @override + String libraryScanStarted({required Object title}) => + 'Scansione libreria iniziata per "${title}"'; + @override + String libraryScanFailed({required Object error}) => + 'Impossibile eseguire scansione della libreria: ${error}'; + @override + String metadataRefreshing({required Object title}) => + 'Aggiornamento metadati per "${title}"...'; + @override + String metadataRefreshStarted({required Object title}) => + 'Aggiornamento metadati per "${title}"'; + @override + String metadataRefreshFailed({required Object error}) => + 'Errore aggiornamento metadati: ${error}'; + @override + String get noPlexToken => 'Nessun token Plex trovato. Riesegui l\'accesso.'; + @override + String get logoutConfirm => 'Sei sicuro di volerti disconnettere?'; + @override + String get noSeasonsFound => 'Nessuna stagione trovata'; + @override + String get noEpisodesFound => 'Nessun episodio trovato nella prima stagione'; + @override + String get noEpisodesFoundGeneral => 'Nessun episodio trovato'; + @override + String get noResultsFound => 'Nessun risultato'; + @override + String sleepTimerSet({required Object label}) => + 'Imposta timer spegnimento per ${label}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Impossibile passare a ${displayName}'; + @override + String get noItemsAvailable => 'Nessun elemento disponibile'; + @override + String get failedToCreatePlayQueue => + 'Impossibile creare la coda di riproduzione'; + @override + String get failedToCreatePlayQueueNoItems => + 'Impossibile creare la coda di riproduzione - nessun elemento'; + @override + String failedPlayback({required Object action, required Object error}) => + 'Impossibile ${action}: ${error}'; + @override + String get youAreOffline => 'Sei offline'; + @override + String get offlineFeatureUnavailable => + 'Questa funzione richiede una connessione internet. I tuoi download e le impostazioni sono ancora disponibili.'; } // Path: profile class _StringsProfileIt implements _StringsProfileEn { - _StringsProfileIt._(this._root); + _StringsProfileIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get noUsersAvailable => 'Nessun utente disponibile'; + // Translations + @override + String get noUsersAvailable => 'Nessun utente disponibile'; } // Path: subtitlingStyling class _StringsSubtitlingStylingIt implements _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingIt._(this._root); + _StringsSubtitlingStylingIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get stylingOptions => 'Opzioni stile'; - @override String get fontSize => 'Dimensione'; - @override String get textColor => 'Colore testo'; - @override String get borderSize => 'Dimensione bordo'; - @override String get borderColor => 'Colore bordo'; - @override String get backgroundOpacity => 'Opacità sfondo'; - @override String get backgroundColor => 'Colore sfondo'; + // Translations + @override + String get stylingOptions => 'Opzioni stile'; + @override + String get fontSize => 'Dimensione'; + @override + String get textColor => 'Colore testo'; + @override + String get borderSize => 'Dimensione bordo'; + @override + String get borderColor => 'Colore bordo'; + @override + String get backgroundOpacity => 'Opacità sfondo'; + @override + String get backgroundColor => 'Colore sfondo'; } // Path: dialog class _StringsDialogIt implements _StringsDialogEn { - _StringsDialogIt._(this._root); + _StringsDialogIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get confirmAction => 'Conferma azione'; - @override String get areYouSure => 'Sei sicuro di voler eseguire questa azione?'; - @override String get cancel => 'Cancella'; - @override String get playNow => 'Riproduci ora'; + // Translations + @override + String get confirmAction => 'Conferma azione'; + @override + String get areYouSure => 'Sei sicuro di voler eseguire questa azione?'; + @override + String get cancel => 'Cancella'; + @override + String get playNow => 'Riproduci ora'; } // Path: discover class _StringsDiscoverIt implements _StringsDiscoverEn { - _StringsDiscoverIt._(this._root); + _StringsDiscoverIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Esplora'; - @override String get switchProfile => 'Cambia profilo'; - @override String get switchServer => 'Cambia server'; - @override String get logout => 'Disconnetti'; - @override String get noContentAvailable => 'Nessun contenuto disponibile'; - @override String get addMediaToLibraries => 'Aggiungi alcuni file multimediali alle tue librerie'; - @override String get continueWatching => 'Continua a guardare'; - @override String get recentlyAdded => 'Aggiunti di recente'; - @override String get play => 'Riproduci'; - @override String get resume => 'Riprendi'; - @override String playEpisode({required Object season, required Object episode}) => 'Riproduci S${season}, E${episode}'; - @override String resumeEpisode({required Object season, required Object episode}) => 'Riprendi S${season}, E${episode}'; - @override String get pause => 'Pausa'; - @override String get overview => 'Panoramica'; - @override String get cast => 'Attori'; - @override String get seasons => 'Stagioni'; - @override String get studio => 'Studio'; - @override String get rating => 'Classificazione'; - @override String get watched => 'Guardato'; - @override String episodeCount({required Object count}) => '${count} episodi'; - @override String watchedProgress({required Object watched, required Object total}) => '${watched}/${total} guardati'; - @override String get movie => 'Film'; - @override String get tvShow => 'Serie TV'; - @override String minutesLeft({required Object minutes}) => '${minutes} minuti rimanenti'; + // Translations + @override + String get title => 'Esplora'; + @override + String get switchProfile => 'Cambia profilo'; + @override + String get switchServer => 'Cambia server'; + @override + String get logout => 'Disconnetti'; + @override + String get noContentAvailable => 'Nessun contenuto disponibile'; + @override + String get addMediaToLibraries => + 'Aggiungi alcuni file multimediali alle tue librerie'; + @override + String get continueWatching => 'Continua a guardare'; + @override + String get recentlyAdded => 'Aggiunti di recente'; + @override + String get play => 'Riproduci'; + @override + String get resume => 'Riprendi'; + @override + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String get pause => 'Pausa'; + @override + String get overview => 'Panoramica'; + @override + String get cast => 'Attori'; + @override + String get seasons => 'Stagioni'; + @override + String get studio => 'Studio'; + @override + String get rating => 'Classificazione'; + @override + String get watched => 'Guardato'; + @override + String episodeCount({required Object count}) => '${count} episodi'; + @override + String watchedProgress({required Object watched, required Object total}) => + '${watched}/${total} guardati'; + @override + String get movie => 'Film'; + @override + String get tvShow => 'Serie TV'; + @override + String minutesLeft({required Object minutes}) => + '${minutes} minuti rimanenti'; } // Path: errors class _StringsErrorsIt implements _StringsErrorsEn { - _StringsErrorsIt._(this._root); + _StringsErrorsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String searchFailed({required Object error}) => 'Ricerca fallita: ${error}'; - @override String connectionTimeout({required Object context}) => 'Timeout connessione durante caricamento di ${context}'; - @override String get connectionFailed => 'Impossibile connettersi al server Plex.'; - @override String failedToLoad({required Object context, required Object error}) => 'Impossibile caricare ${context}: ${error}'; - @override String get noClientAvailable => 'Nessun client disponibile'; - @override String authenticationFailed({required Object error}) => 'Autenticazione fallita: ${error}'; - @override String get couldNotLaunchUrl => 'Impossibile avviare URL di autenticazione'; - @override String get pleaseEnterToken => 'Inserisci token'; - @override String get invalidToken => 'Token non valido'; - @override String failedToVerifyToken({required Object error}) => 'Verifica token fallita: ${error}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Impossibile passare a ${displayName}'; - @override String get connectionFailedGeneric => 'Connessione fallita'; + // Translations + @override + String searchFailed({required Object error}) => 'Ricerca fallita: ${error}'; + @override + String connectionTimeout({required Object context}) => + 'Timeout connessione durante caricamento di ${context}'; + @override + String get connectionFailed => 'Impossibile connettersi al server Plex.'; + @override + String failedToLoad({required Object context, required Object error}) => + 'Impossibile caricare ${context}: ${error}'; + @override + String get noClientAvailable => 'Nessun client disponibile'; + @override + String authenticationFailed({required Object error}) => + 'Autenticazione fallita: ${error}'; + @override + String get couldNotLaunchUrl => 'Impossibile avviare URL di autenticazione'; + @override + String get pleaseEnterToken => 'Inserisci token'; + @override + String get invalidToken => 'Token non valido'; + @override + String failedToVerifyToken({required Object error}) => + 'Verifica token fallita: ${error}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Impossibile passare a ${displayName}'; + @override + String get connectionFailedGeneric => 'Connessione fallita'; } // Path: libraries class _StringsLibrariesIt implements _StringsLibrariesEn { - _StringsLibrariesIt._(this._root); + _StringsLibrariesIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Librerie'; - @override String get scanLibraryFiles => 'Scansiona file libreria'; - @override String get scanLibrary => 'Scansiona libreria'; - @override String get analyze => 'Analizza'; - @override String get analyzeLibrary => 'Analizza libreria'; - @override String get refreshMetadata => 'Aggiorna metadati'; - @override String get emptyTrash => 'Svuota cestino'; - @override String emptyingTrash({required Object title}) => 'Svuotamento cestino per "${title}"...'; - @override String trashEmptied({required Object title}) => 'Cestino svuotato per "${title}"'; - @override String failedToEmptyTrash({required Object error}) => 'Impossibile svuotare cestino: ${error}'; - @override String analyzing({required Object title}) => 'Analisi "${title}"...'; - @override String analysisStarted({required Object title}) => 'Analisi iniziata per "${title}"'; - @override String failedToAnalyze({required Object error}) => 'Impossibile analizzare libreria: ${error}'; - @override String get noLibrariesFound => 'Nessuna libreria trovata'; - @override String get thisLibraryIsEmpty => 'Questa libreria è vuota'; - @override String get all => 'Tutto'; - @override String get clearAll => 'Cancella tutto'; - @override String scanLibraryConfirm({required Object title}) => 'Sei sicuro di voler scansionare "${title}"?'; - @override String analyzeLibraryConfirm({required Object title}) => 'Sei sicuro di voler analizzare "${title}"?'; - @override String refreshMetadataConfirm({required Object title}) => 'Sei sicuro di voler aggiornare i metadati per "${title}"?'; - @override String emptyTrashConfirm({required Object title}) => 'Sei sicuro di voler svuotare il cestino per "${title}"?'; - @override String get manageLibraries => 'Gestisci librerie'; - @override String get sort => 'Ordina'; - @override String get sortBy => 'Ordina per'; - @override String get filters => 'Filtri'; - @override String loadingLibraryWithCount({required Object count}) => 'Caricamento librerie... (${count} oggetti caricati)'; - @override String get confirmActionMessage => 'Sei sicuro di voler eseguire questa azione?'; - @override String get showLibrary => 'Mostra libreria'; - @override String get hideLibrary => 'Nascondi libreria'; - @override String get libraryOptions => 'Opzioni libreria'; - @override String get content => 'contenuto della libreria'; - @override String get selectLibrary => 'Seleziona libreria'; - @override String filtersWithCount({required Object count}) => 'Filtri (${count})'; - @override String get noRecommendations => 'Nessun consiglio disponibile'; - @override String get noCollections => 'Nessuna raccolta in questa libreria'; - @override String get noFoldersFound => 'Nessuna cartella trovata'; - @override String get folders => 'cartelle'; - @override late final _StringsLibrariesTabsIt tabs = _StringsLibrariesTabsIt._(_root); - @override late final _StringsLibrariesGroupingsIt groupings = _StringsLibrariesGroupingsIt._(_root); + // Translations + @override + String get title => 'Librerie'; + @override + String get scanLibraryFiles => 'Scansiona file libreria'; + @override + String get scanLibrary => 'Scansiona libreria'; + @override + String get analyze => 'Analizza'; + @override + String get analyzeLibrary => 'Analizza libreria'; + @override + String get refreshMetadata => 'Aggiorna metadati'; + @override + String get emptyTrash => 'Svuota cestino'; + @override + String emptyingTrash({required Object title}) => + 'Svuotamento cestino per "${title}"...'; + @override + String trashEmptied({required Object title}) => + 'Cestino svuotato per "${title}"'; + @override + String failedToEmptyTrash({required Object error}) => + 'Impossibile svuotare cestino: ${error}'; + @override + String analyzing({required Object title}) => 'Analisi "${title}"...'; + @override + String analysisStarted({required Object title}) => + 'Analisi iniziata per "${title}"'; + @override + String failedToAnalyze({required Object error}) => + 'Impossibile analizzare libreria: ${error}'; + @override + String get noLibrariesFound => 'Nessuna libreria trovata'; + @override + String get thisLibraryIsEmpty => 'Questa libreria è vuota'; + @override + String get all => 'Tutto'; + @override + String get clearAll => 'Cancella tutto'; + @override + String scanLibraryConfirm({required Object title}) => + 'Sei sicuro di voler scansionare "${title}"?'; + @override + String analyzeLibraryConfirm({required Object title}) => + 'Sei sicuro di voler analizzare "${title}"?'; + @override + String refreshMetadataConfirm({required Object title}) => + 'Sei sicuro di voler aggiornare i metadati per "${title}"?'; + @override + String emptyTrashConfirm({required Object title}) => + 'Sei sicuro di voler svuotare il cestino per "${title}"?'; + @override + String get manageLibraries => 'Gestisci librerie'; + @override + String get sort => 'Ordina'; + @override + String get sortBy => 'Ordina per'; + @override + String get filters => 'Filtri'; + @override + String loadingLibraryWithCount({required Object count}) => + 'Caricamento librerie... (${count} oggetti caricati)'; + @override + String get confirmActionMessage => + 'Sei sicuro di voler eseguire questa azione?'; + @override + String get showLibrary => 'Mostra libreria'; + @override + String get hideLibrary => 'Nascondi libreria'; + @override + String get libraryOptions => 'Opzioni libreria'; + @override + String get content => 'contenuto della libreria'; + @override + String get selectLibrary => 'Seleziona libreria'; + @override + String filtersWithCount({required Object count}) => 'Filtri (${count})'; + @override + String get noRecommendations => 'Nessun consiglio disponibile'; + @override + String get noCollections => 'Nessuna raccolta in questa libreria'; + @override + String get noFoldersFound => 'Nessuna cartella trovata'; + @override + String get folders => 'cartelle'; + @override + late final _StringsLibrariesTabsIt tabs = _StringsLibrariesTabsIt._(_root); + @override + late final _StringsLibrariesGroupingsIt groupings = + _StringsLibrariesGroupingsIt._(_root); } // Path: about class _StringsAboutIt implements _StringsAboutEn { - _StringsAboutIt._(this._root); + _StringsAboutIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Informazioni'; - @override String get openSourceLicenses => 'Licenze Open Source'; - @override String versionLabel({required Object version}) => 'Versione ${version}'; - @override String get appDescription => 'Un bellissimo client Plex per Flutter'; - @override String get viewLicensesDescription => 'Visualizza le licenze delle librerie di terze parti'; + // Translations + @override + String get title => 'Informazioni'; + @override + String get openSourceLicenses => 'Licenze Open Source'; + @override + String versionLabel({required Object version}) => 'Versione ${version}'; + @override + String get appDescription => 'Un bellissimo client Plex per Flutter'; + @override + String get viewLicensesDescription => + 'Visualizza le licenze delle librerie di terze parti'; } // Path: serverSelection class _StringsServerSelectionIt implements _StringsServerSelectionEn { - _StringsServerSelectionIt._(this._root); + _StringsServerSelectionIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get connectingToServer => 'Connessione al server...'; - @override String get connectingToAllServers => 'Connessione a tutti i server...'; - @override String get connectToAllServers => 'Connetti a tutti i server'; - @override String get allServerConnectionsFailed => 'Impossibile connettersi a nessun server. Controlla la tua rete e riprova.'; - @override String foundServers({required Object count}) => 'Trovati ${count} server'; - @override String get orSelectIndividually => 'Oppure seleziona un server singolarmente:'; - @override String get serverDebugCopied => 'Dati di debug del server copiati negli appunti'; - @override String get copyDebugData => 'Copia dati di debug'; - @override String get noServersFound => 'Nessun server trovato'; - @override String noServersFoundForAccount({required Object username, required Object email}) => 'Nessun server trovato per ${username} (${email})'; - @override String malformedServerData({required Object count}) => 'Trovato ${count} server con dati difettosi. Nessun server valido disponibile.'; - @override String get incompleteServerInfo => 'Alcuni server presentano informazioni incomplete e sono stati ignorati. Controlla il tuo account Plex.tv.'; - @override String get incompleteConnectionInfo => 'Le informazioni di connessione al server sono incomplete. Riprova.'; - @override String malformedServerInfo({required Object message}) => 'Le informazioni sul server sono errate: ${message}'; - @override String get networkConnectionFailed => 'Connessione di rete non riuscita. Controlla la tua connessione Internet e riprova.'; - @override String get authenticationFailed => 'Autenticazione fallita. Effettua nuovamente l\'accesso.'; - @override String get plexServiceUnavailable => 'Servizio Plex non disponibile. Riprova più tardi.'; - @override String failedToLoadServers({required Object error}) => 'Impossibile caricare i server: ${error}'; + // Translations + @override + String get connectingToServer => 'Connessione al server...'; + @override + String get connectingToAllServers => 'Connessione a tutti i server...'; + @override + String get connectToAllServers => 'Connetti a tutti i server'; + @override + String get allServerConnectionsFailed => + 'Impossibile connettersi a nessun server. Controlla la tua rete e riprova.'; + @override + String foundServers({required Object count}) => 'Trovati ${count} server'; + @override + String get orSelectIndividually => + 'Oppure seleziona un server singolarmente:'; + @override + String get serverDebugCopied => + 'Dati di debug del server copiati negli appunti'; + @override + String get copyDebugData => 'Copia dati di debug'; + @override + String get noServersFound => 'Nessun server trovato'; + @override + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => 'Nessun server trovato per ${username} (${email})'; + @override + String malformedServerData({required Object count}) => + 'Trovato ${count} server con dati difettosi. Nessun server valido disponibile.'; + @override + String get incompleteServerInfo => + 'Alcuni server presentano informazioni incomplete e sono stati ignorati. Controlla il tuo account Plex.tv.'; + @override + String get incompleteConnectionInfo => + 'Le informazioni di connessione al server sono incomplete. Riprova.'; + @override + String malformedServerInfo({required Object message}) => + 'Le informazioni sul server sono errate: ${message}'; + @override + String get networkConnectionFailed => + 'Connessione di rete non riuscita. Controlla la tua connessione Internet e riprova.'; + @override + String get authenticationFailed => + 'Autenticazione fallita. Effettua nuovamente l\'accesso.'; + @override + String get plexServiceUnavailable => + 'Servizio Plex non disponibile. Riprova più tardi.'; + @override + String failedToLoadServers({required Object error}) => + 'Impossibile caricare i server: ${error}'; } // Path: hubDetail class _StringsHubDetailIt implements _StringsHubDetailEn { - _StringsHubDetailIt._(this._root); + _StringsHubDetailIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Titolo'; - @override String get releaseYear => 'Anno rilascio'; - @override String get dateAdded => 'Data aggiunta'; - @override String get rating => 'Valutazione'; - @override String get noItemsFound => 'Nessun elemento trovato'; + // Translations + @override + String get title => 'Titolo'; + @override + String get releaseYear => 'Anno rilascio'; + @override + String get dateAdded => 'Data aggiunta'; + @override + String get rating => 'Valutazione'; + @override + String get noItemsFound => 'Nessun elemento trovato'; } // Path: logs class _StringsLogsIt implements _StringsLogsEn { - _StringsLogsIt._(this._root); + _StringsLogsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Registro'; - @override String get clearLogs => 'Cancella log'; - @override String get copyLogs => 'Copia log'; - @override String get exportLogs => 'Esporta log'; - @override String get noLogsToShow => 'Nessun log da mostrare'; - @override String get error => 'Errore:'; - @override String get stackTrace => 'Traccia dello stack:'; + // Translations + @override + String get title => 'Registro'; + @override + String get clearLogs => 'Cancella log'; + @override + String get copyLogs => 'Copia log'; + @override + String get exportLogs => 'Esporta log'; + @override + String get noLogsToShow => 'Nessun log da mostrare'; + @override + String get error => 'Errore:'; + @override + String get stackTrace => 'Traccia dello stack:'; } // Path: licenses class _StringsLicensesIt implements _StringsLicensesEn { - _StringsLicensesIt._(this._root); + _StringsLicensesIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get relatedPackages => 'Pacchetti correlati'; - @override String get license => 'Licenza'; - @override String licenseNumber({required Object number}) => 'Licenza ${number}'; - @override String licensesCount({required Object count}) => '${count} licenze'; + // Translations + @override + String get relatedPackages => 'Pacchetti correlati'; + @override + String get license => 'Licenza'; + @override + String licenseNumber({required Object number}) => 'Licenza ${number}'; + @override + String licensesCount({required Object count}) => '${count} licenze'; } // Path: navigation class _StringsNavigationIt implements _StringsNavigationEn { - _StringsNavigationIt._(this._root); + _StringsNavigationIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get home => 'Home'; - @override String get search => 'Cerca'; - @override String get libraries => 'Librerie'; - @override String get settings => 'Impostazioni'; + // Translations + @override + String get home => 'Home'; + @override + String get search => 'Cerca'; + @override + String get libraries => 'Librerie'; + @override + String get settings => 'Impostazioni'; + @override + String get downloads => 'Download'; +} + +// Path: downloads +class _StringsDownloadsIt implements _StringsDownloadsEn { + _StringsDownloadsIt._(this._root); + + @override + final _StringsIt _root; // ignore: unused_field + + // Translations + @override + String get title => 'Download'; + @override + String get manage => 'Gestisci'; + @override + String get tvShows => 'Serie TV'; + @override + String get movies => 'Film'; + @override + String get queue => 'Coda'; + @override + String get noDownloads => 'Nessun download'; + @override + String get noDownloadsDescription => + 'I contenuti scaricati appariranno qui per la visualizzazione offline'; + @override + String get browseContent => 'Sfoglia contenuti'; + @override + String get downloading => 'Download in corso'; + @override + String get downloaded => 'Scaricato'; + @override + String get paused => 'In pausa'; + @override + String get failed => 'Fallito'; + @override + String get queued => 'In coda'; + @override + String get downloadNow => 'Scarica'; + @override + String get deleteDownload => 'Elimina download'; + @override + String get cancelDownload => 'Annulla download'; + @override + String get pauseDownload => 'Metti in pausa download'; + @override + String get resumeDownload => 'Riprendi download'; + @override + String get retryDownload => 'Riprova download'; + @override + String get downloadComplete => 'Download completato'; + @override + String get downloadFailed => 'Download fallito'; + @override + String get downloadCancelled => 'Download annullato'; + @override + String get downloadQueued => 'Download in coda'; + @override + String episodesQueued({required Object count}) => + '${count} episodi in coda per il download'; + @override + String get downloadDeleted => 'Download eliminato'; + @override + String get deleteConfirmTitle => 'Eliminare il download?'; + @override + String deleteConfirm({required Object title}) => + 'Sei sicuro di voler eliminare "${title}"? Il file scaricato verrà rimosso dal tuo dispositivo.'; + @override + String get deleteConfirmMessage => + 'Il file scaricato verrà rimosso dal tuo dispositivo.'; + @override + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => 'Eliminazione di ${title}... (${current} di ${total})'; + @override + String deletingSimple({required Object title}) => + 'Eliminazione di ${title}...'; + @override + String get storageUsed => 'Spazio utilizzato'; + @override + String get availableOffline => 'Disponibile offline'; + @override + String get downloadingSeason => 'Download stagione in corso'; + @override + String get downloadingEpisode => 'Download episodio in corso'; +} + +// Path: offline +class _StringsOfflineIt implements _StringsOfflineEn { + _StringsOfflineIt._(this._root); + + @override + final _StringsIt _root; // ignore: unused_field + + // Translations + @override + String get youAreOffline => 'Sei offline'; + @override + String get offlineMode => 'Modalità offline'; + @override + String get backOnline => 'Di nuovo online'; + @override + String get showingDownloadedContent => + 'Visualizzazione solo contenuti scaricati'; + @override + String get noInternetConnection => 'Nessuna connessione internet'; + @override + String get serversUnreachable => 'Server Plex non raggiungibili'; + @override + String get connectToDownload => + 'Connettiti a internet per scaricare contenuti'; } // Path: playlists class _StringsPlaylistsIt implements _StringsPlaylistsEn { - _StringsPlaylistsIt._(this._root); + _StringsPlaylistsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Playlist'; - @override String get noPlaylists => 'Nessuna playlist trovata'; - @override String get create => 'Crea playlist'; - @override String get newPlaylist => 'Nuova playlist'; - @override String get playlistName => 'Nome playlist'; - @override String get enterPlaylistName => 'Inserisci nome playlist'; - @override String get edit => 'Modifica playlist'; - @override String get delete => 'Elimina playlist'; - @override String get addTo => 'Aggiungi a playlist'; - @override String get addItems => 'Aggiungi elementi'; - @override String get removeItem => 'Rimuovi da playlist'; - @override String get clearPlaylist => 'Svuota playlist'; - @override String get playAll => 'Riproduci tutto'; - @override String get shuffle => 'Casuale'; - @override String get smartPlaylist => 'Playlist intelligente'; - @override String get regularPlaylist => 'Playlist normale'; - @override String itemCount({required Object count}) => '${count} elementi'; - @override String get oneItem => '1 elemento'; - @override String get emptyPlaylist => 'Questa playlist è vuota'; - @override String get deleteConfirm => 'Eliminare playlist?'; - @override String deleteMessage({required Object name}) => 'Sei sicuro di voler eliminare "${name}"?'; - @override String get created => 'Playlist creata'; - @override String get updated => 'Playlist aggiornata'; - @override String get deleted => 'Playlist eliminata'; - @override String get itemAdded => 'Aggiunto alla playlist'; - @override String get itemRemoved => 'Rimosso dalla playlist'; - @override String get selectPlaylist => 'Seleziona playlist'; - @override String get createNewPlaylist => 'Crea nuova playlist'; - @override String get errorCreating => 'Errore durante la creazione della playlist'; - @override String get errorDeleting => 'Errore durante l\'eliminazione della playlist'; - @override String get errorLoading => 'Errore durante il caricamento delle playlist'; - @override String get errorAdding => 'Errore durante l\'aggiunta alla playlist'; - @override String get errorReordering => 'Errore durante il riordino dell\'elemento della playlist'; - @override String get errorRemoving => 'Errore durante la rimozione dalla playlist'; - @override String get playlist => 'Playlist'; + // Translations + @override + String get title => 'Playlist'; + @override + String get noPlaylists => 'Nessuna playlist trovata'; + @override + String get create => 'Crea playlist'; + @override + String get newPlaylist => 'Nuova playlist'; + @override + String get playlistName => 'Nome playlist'; + @override + String get enterPlaylistName => 'Inserisci nome playlist'; + @override + String get edit => 'Modifica playlist'; + @override + String get delete => 'Elimina playlist'; + @override + String get addTo => 'Aggiungi a playlist'; + @override + String get addItems => 'Aggiungi elementi'; + @override + String get removeItem => 'Rimuovi da playlist'; + @override + String get clearPlaylist => 'Svuota playlist'; + @override + String get playAll => 'Riproduci tutto'; + @override + String get shuffle => 'Casuale'; + @override + String get smartPlaylist => 'Playlist intelligente'; + @override + String get regularPlaylist => 'Playlist normale'; + @override + String itemCount({required Object count}) => '${count} elementi'; + @override + String get oneItem => '1 elemento'; + @override + String get emptyPlaylist => 'Questa playlist è vuota'; + @override + String get deleteConfirm => 'Eliminare playlist?'; + @override + String deleteMessage({required Object name}) => + 'Sei sicuro di voler eliminare "${name}"?'; + @override + String get created => 'Playlist creata'; + @override + String get updated => 'Playlist aggiornata'; + @override + String get deleted => 'Playlist eliminata'; + @override + String get itemAdded => 'Aggiunto alla playlist'; + @override + String get itemRemoved => 'Rimosso dalla playlist'; + @override + String get selectPlaylist => 'Seleziona playlist'; + @override + String get createNewPlaylist => 'Crea nuova playlist'; + @override + String get errorCreating => 'Errore durante la creazione della playlist'; + @override + String get errorDeleting => 'Errore durante l\'eliminazione della playlist'; + @override + String get errorLoading => 'Errore durante il caricamento delle playlist'; + @override + String get errorAdding => 'Errore durante l\'aggiunta alla playlist'; + @override + String get errorReordering => + 'Errore durante il riordino dell\'elemento della playlist'; + @override + String get errorRemoving => 'Errore durante la rimozione dalla playlist'; + @override + String get playlist => 'Playlist'; } // Path: collections class _StringsCollectionsIt implements _StringsCollectionsEn { - _StringsCollectionsIt._(this._root); + _StringsCollectionsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get title => 'Raccolte'; - @override String get collection => 'Raccolta'; - @override String get empty => 'La raccolta è vuota'; - @override String get noItems => 'Nessun elemento in questa raccolta'; - @override String get unknownLibrarySection => 'Impossibile eliminare: sezione libreria sconosciuta'; - @override String get deleteCollection => 'Elimina raccolta'; - @override String deleteConfirm({required Object title}) => 'Sei sicuro di voler eliminare "${title}"? Questa azione non può essere annullata.'; - @override String get deleted => 'Raccolta eliminata'; - @override String get deleteFailed => 'Impossibile eliminare la raccolta'; - @override String deleteFailedWithError({required Object error}) => 'Impossibile eliminare la raccolta: ${error}'; - @override String failedToLoadItems({required Object error}) => 'Impossibile caricare gli elementi della raccolta: ${error}'; - @override String get addTo => 'Aggiungi alla raccolta'; - @override String get selectCollection => 'Seleziona raccolta'; - @override String get createNewCollection => 'Crea nuova raccolta'; - @override String get collectionName => 'Nome raccolta'; - @override String get enterCollectionName => 'Inserisci nome raccolta'; - @override String get addedToCollection => 'Aggiunto alla raccolta'; - @override String get errorAddingToCollection => 'Errore nell\'aggiunta alla raccolta'; - @override String get created => 'Raccolta creata'; - @override String get removeFromCollection => 'Rimuovi dalla raccolta'; - @override String removeFromCollectionConfirm({required Object title}) => 'Rimuovere "${title}" da questa raccolta?'; - @override String get removedFromCollection => 'Rimosso dalla raccolta'; - @override String get removeFromCollectionFailed => 'Impossibile rimuovere dalla raccolta'; - @override String removeFromCollectionError({required Object error}) => 'Errore durante la rimozione dalla raccolta: ${error}'; + // Translations + @override + String get title => 'Raccolte'; + @override + String get collection => 'Raccolta'; + @override + String get empty => 'La raccolta è vuota'; + @override + String get noItems => 'Nessun elemento in questa raccolta'; + @override + String get unknownLibrarySection => + 'Impossibile eliminare: sezione libreria sconosciuta'; + @override + String get deleteCollection => 'Elimina raccolta'; + @override + String deleteConfirm({required Object title}) => + 'Sei sicuro di voler eliminare "${title}"? Questa azione non può essere annullata.'; + @override + String get deleted => 'Raccolta eliminata'; + @override + String get deleteFailed => 'Impossibile eliminare la raccolta'; + @override + String deleteFailedWithError({required Object error}) => + 'Impossibile eliminare la raccolta: ${error}'; + @override + String failedToLoadItems({required Object error}) => + 'Impossibile caricare gli elementi della raccolta: ${error}'; + @override + String get addTo => 'Aggiungi alla raccolta'; + @override + String get selectCollection => 'Seleziona raccolta'; + @override + String get createNewCollection => 'Crea nuova raccolta'; + @override + String get collectionName => 'Nome raccolta'; + @override + String get enterCollectionName => 'Inserisci nome raccolta'; + @override + String get addedToCollection => 'Aggiunto alla raccolta'; + @override + String get errorAddingToCollection => 'Errore nell\'aggiunta alla raccolta'; + @override + String get created => 'Raccolta creata'; + @override + String get removeFromCollection => 'Rimuovi dalla raccolta'; + @override + String removeFromCollectionConfirm({required Object title}) => + 'Rimuovere "${title}" da questa raccolta?'; + @override + String get removedFromCollection => 'Rimosso dalla raccolta'; + @override + String get removeFromCollectionFailed => + 'Impossibile rimuovere dalla raccolta'; + @override + String removeFromCollectionError({required Object error}) => + 'Errore durante la rimozione dalla raccolta: ${error}'; } // Path: libraries.tabs class _StringsLibrariesTabsIt implements _StringsLibrariesTabsEn { - _StringsLibrariesTabsIt._(this._root); + _StringsLibrariesTabsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get recommended => 'Consigliati'; - @override String get browse => 'Esplora'; - @override String get collections => 'Raccolte'; - @override String get playlists => 'Playlist'; + // Translations + @override + String get recommended => 'Consigliati'; + @override + String get browse => 'Esplora'; + @override + String get collections => 'Raccolte'; + @override + String get playlists => 'Playlist'; } // Path: libraries.groupings class _StringsLibrariesGroupingsIt implements _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsIt._(this._root); + _StringsLibrariesGroupingsIt._(this._root); - @override final _StringsIt _root; // ignore: unused_field + @override + final _StringsIt _root; // ignore: unused_field - // Translations - @override String get all => 'Tutti'; - @override String get movies => 'Film'; - @override String get shows => 'Serie TV'; - @override String get seasons => 'Stagioni'; - @override String get episodes => 'Episodi'; - @override String get folders => 'Cartelle'; + // Translations + @override + String get all => 'Tutti'; + @override + String get movies => 'Film'; + @override + String get shows => 'Serie TV'; + @override + String get seasons => 'Stagioni'; + @override + String get episodes => 'Episodi'; + @override + String get folders => 'Cartelle'; } // Path: class _StringsNl implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsNl.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.nl, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsNl.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.nl, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); - @override late final _StringsNl _root = this; // ignore: unused_field + @override + late final _StringsNl _root = this; // ignore: unused_field - // Translations - @override late final _StringsAppNl app = _StringsAppNl._(_root); - @override late final _StringsAuthNl auth = _StringsAuthNl._(_root); - @override late final _StringsCommonNl common = _StringsCommonNl._(_root); - @override late final _StringsScreensNl screens = _StringsScreensNl._(_root); - @override late final _StringsUpdateNl update = _StringsUpdateNl._(_root); - @override late final _StringsSettingsNl settings = _StringsSettingsNl._(_root); - @override late final _StringsSearchNl search = _StringsSearchNl._(_root); - @override late final _StringsHotkeysNl hotkeys = _StringsHotkeysNl._(_root); - @override late final _StringsPinEntryNl pinEntry = _StringsPinEntryNl._(_root); - @override late final _StringsFileInfoNl fileInfo = _StringsFileInfoNl._(_root); - @override late final _StringsMediaMenuNl mediaMenu = _StringsMediaMenuNl._(_root); - @override late final _StringsAccessibilityNl accessibility = _StringsAccessibilityNl._(_root); - @override late final _StringsTooltipsNl tooltips = _StringsTooltipsNl._(_root); - @override late final _StringsVideoControlsNl videoControls = _StringsVideoControlsNl._(_root); - @override late final _StringsUserStatusNl userStatus = _StringsUserStatusNl._(_root); - @override late final _StringsMessagesNl messages = _StringsMessagesNl._(_root); - @override late final _StringsProfileNl profile = _StringsProfileNl._(_root); - @override late final _StringsSubtitlingStylingNl subtitlingStyling = _StringsSubtitlingStylingNl._(_root); - @override late final _StringsDialogNl dialog = _StringsDialogNl._(_root); - @override late final _StringsDiscoverNl discover = _StringsDiscoverNl._(_root); - @override late final _StringsErrorsNl errors = _StringsErrorsNl._(_root); - @override late final _StringsLibrariesNl libraries = _StringsLibrariesNl._(_root); - @override late final _StringsAboutNl about = _StringsAboutNl._(_root); - @override late final _StringsServerSelectionNl serverSelection = _StringsServerSelectionNl._(_root); - @override late final _StringsHubDetailNl hubDetail = _StringsHubDetailNl._(_root); - @override late final _StringsLogsNl logs = _StringsLogsNl._(_root); - @override late final _StringsLicensesNl licenses = _StringsLicensesNl._(_root); - @override late final _StringsNavigationNl navigation = _StringsNavigationNl._(_root); - @override late final _StringsPlaylistsNl playlists = _StringsPlaylistsNl._(_root); - @override late final _StringsCollectionsNl collections = _StringsCollectionsNl._(_root); + // Translations + @override + late final _StringsAppNl app = _StringsAppNl._(_root); + @override + late final _StringsAuthNl auth = _StringsAuthNl._(_root); + @override + late final _StringsCommonNl common = _StringsCommonNl._(_root); + @override + late final _StringsScreensNl screens = _StringsScreensNl._(_root); + @override + late final _StringsUpdateNl update = _StringsUpdateNl._(_root); + @override + late final _StringsSettingsNl settings = _StringsSettingsNl._(_root); + @override + late final _StringsSearchNl search = _StringsSearchNl._(_root); + @override + late final _StringsHotkeysNl hotkeys = _StringsHotkeysNl._(_root); + @override + late final _StringsPinEntryNl pinEntry = _StringsPinEntryNl._(_root); + @override + late final _StringsFileInfoNl fileInfo = _StringsFileInfoNl._(_root); + @override + late final _StringsMediaMenuNl mediaMenu = _StringsMediaMenuNl._(_root); + @override + late final _StringsAccessibilityNl accessibility = _StringsAccessibilityNl._( + _root, + ); + @override + late final _StringsTooltipsNl tooltips = _StringsTooltipsNl._(_root); + @override + late final _StringsVideoControlsNl videoControls = _StringsVideoControlsNl._( + _root, + ); + @override + late final _StringsUserStatusNl userStatus = _StringsUserStatusNl._(_root); + @override + late final _StringsMessagesNl messages = _StringsMessagesNl._(_root); + @override + late final _StringsProfileNl profile = _StringsProfileNl._(_root); + @override + late final _StringsSubtitlingStylingNl subtitlingStyling = + _StringsSubtitlingStylingNl._(_root); + @override + late final _StringsDialogNl dialog = _StringsDialogNl._(_root); + @override + late final _StringsDiscoverNl discover = _StringsDiscoverNl._(_root); + @override + late final _StringsErrorsNl errors = _StringsErrorsNl._(_root); + @override + late final _StringsLibrariesNl libraries = _StringsLibrariesNl._(_root); + @override + late final _StringsAboutNl about = _StringsAboutNl._(_root); + @override + late final _StringsServerSelectionNl serverSelection = + _StringsServerSelectionNl._(_root); + @override + late final _StringsHubDetailNl hubDetail = _StringsHubDetailNl._(_root); + @override + late final _StringsLogsNl logs = _StringsLogsNl._(_root); + @override + late final _StringsLicensesNl licenses = _StringsLicensesNl._(_root); + @override + late final _StringsNavigationNl navigation = _StringsNavigationNl._(_root); + @override + late final _StringsDownloadsNl downloads = _StringsDownloadsNl._(_root); + @override + late final _StringsOfflineNl offline = _StringsOfflineNl._(_root); + @override + late final _StringsPlaylistsNl playlists = _StringsPlaylistsNl._(_root); + @override + late final _StringsCollectionsNl collections = _StringsCollectionsNl._(_root); } // Path: app class _StringsAppNl implements _StringsAppEn { - _StringsAppNl._(this._root); + _StringsAppNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Plezy'; - @override String get loading => 'Laden...'; + // Translations + @override + String get title => 'Plezy'; + @override + String get loading => 'Laden...'; } // Path: auth class _StringsAuthNl implements _StringsAuthEn { - _StringsAuthNl._(this._root); + _StringsAuthNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get signInWithPlex => 'Inloggen met Plex'; - @override String get showQRCode => 'Toon QR-code'; - @override String get cancel => 'Annuleren'; - @override String get authenticate => 'Authenticeren'; - @override String get retry => 'Opnieuw proberen'; - @override String get debugEnterToken => 'Debug: Voer Plex Token in'; - @override String get plexTokenLabel => 'Plex Authenticatietoken'; - @override String get plexTokenHint => 'Voer je Plex.tv token in'; - @override String get authenticationTimeout => 'Authenticatie verlopen. Probeer opnieuw.'; - @override String get scanQRCodeInstruction => 'Scan deze QR-code met een apparaat dat is ingelogd op Plex om te authenticeren.'; - @override String get waitingForAuth => 'Wachten op authenticatie...\nVoltooi het inloggen in je browser.'; + // Translations + @override + String get signInWithPlex => 'Inloggen met Plex'; + @override + String get showQRCode => 'Toon QR-code'; + @override + String get cancel => 'Annuleren'; + @override + String get authenticate => 'Authenticeren'; + @override + String get retry => 'Opnieuw proberen'; + @override + String get debugEnterToken => 'Debug: Voer Plex Token in'; + @override + String get plexTokenLabel => 'Plex Authenticatietoken'; + @override + String get plexTokenHint => 'Voer je Plex.tv token in'; + @override + String get authenticationTimeout => + 'Authenticatie verlopen. Probeer opnieuw.'; + @override + String get scanQRCodeInstruction => + 'Scan deze QR-code met een apparaat dat is ingelogd op Plex om te authenticeren.'; + @override + String get waitingForAuth => + 'Wachten op authenticatie...\nVoltooi het inloggen in je browser.'; } // Path: common class _StringsCommonNl implements _StringsCommonEn { - _StringsCommonNl._(this._root); + _StringsCommonNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get cancel => 'Annuleren'; - @override String get save => 'Opslaan'; - @override String get close => 'Sluiten'; - @override String get clear => 'Wissen'; - @override String get reset => 'Resetten'; - @override String get later => 'Later'; - @override String get submit => 'Verzenden'; - @override String get confirm => 'Bevestigen'; - @override String get retry => 'Opnieuw proberen'; - @override String get playNow => 'Nu afspelen'; - @override String get logout => 'Uitloggen'; - @override String get online => 'Online'; - @override String get offline => 'Offline'; - @override String get owned => 'Eigendom'; - @override String get shared => 'Gedeeld'; - @override String get current => 'HUIDIG'; - @override String get unknown => 'Onbekend'; - @override String get refresh => 'Vernieuwen'; - @override String get yes => 'Ja'; - @override String get no => 'Nee'; - @override String get server => 'Server'; - @override String get delete => 'Verwijderen'; - @override String get shuffle => 'Willekeurig'; - @override String get addTo => 'Toevoegen aan...'; + // Translations + @override + String get cancel => 'Annuleren'; + @override + String get save => 'Opslaan'; + @override + String get close => 'Sluiten'; + @override + String get clear => 'Wissen'; + @override + String get reset => 'Resetten'; + @override + String get later => 'Later'; + @override + String get submit => 'Verzenden'; + @override + String get confirm => 'Bevestigen'; + @override + String get retry => 'Opnieuw proberen'; + @override + String get playNow => 'Nu afspelen'; + @override + String get logout => 'Uitloggen'; + @override + String get online => 'Online'; + @override + String get offline => 'Offline'; + @override + String get owned => 'Eigendom'; + @override + String get shared => 'Gedeeld'; + @override + String get current => 'HUIDIG'; + @override + String get unknown => 'Onbekend'; + @override + String get refresh => 'Vernieuwen'; + @override + String get yes => 'Ja'; + @override + String get no => 'Nee'; + @override + String get server => 'Server'; + @override + String get delete => 'Verwijderen'; + @override + String get shuffle => 'Willekeurig'; + @override + String get addTo => 'Toevoegen aan...'; } // Path: screens class _StringsScreensNl implements _StringsScreensEn { - _StringsScreensNl._(this._root); + _StringsScreensNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get licenses => 'Licenties'; - @override String get selectServer => 'Selecteer server'; - @override String get switchProfile => 'Wissel van profiel'; - @override String get subtitleStyling => 'Ondertitel opmaak'; - @override String get search => 'Zoeken'; - @override String get logs => 'Logbestanden'; + // Translations + @override + String get licenses => 'Licenties'; + @override + String get selectServer => 'Selecteer server'; + @override + String get switchProfile => 'Wissel van profiel'; + @override + String get subtitleStyling => 'Ondertitel opmaak'; + @override + String get search => 'Zoeken'; + @override + String get logs => 'Logbestanden'; } // Path: update class _StringsUpdateNl implements _StringsUpdateEn { - _StringsUpdateNl._(this._root); + _StringsUpdateNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get available => 'Update beschikbaar'; - @override String versionAvailable({required Object version}) => 'Versie ${version} is beschikbaar'; - @override String currentVersion({required Object version}) => 'Huidig: ${version}'; - @override String get skipVersion => 'Deze versie overslaan'; - @override String get viewRelease => 'Bekijk release'; - @override String get latestVersion => 'Je hebt de nieuwste versie'; - @override String get checkFailed => 'Kon niet controleren op updates'; + // Translations + @override + String get available => 'Update beschikbaar'; + @override + String versionAvailable({required Object version}) => + 'Versie ${version} is beschikbaar'; + @override + String currentVersion({required Object version}) => 'Huidig: ${version}'; + @override + String get skipVersion => 'Deze versie overslaan'; + @override + String get viewRelease => 'Bekijk release'; + @override + String get latestVersion => 'Je hebt de nieuwste versie'; + @override + String get checkFailed => 'Kon niet controleren op updates'; } // Path: settings class _StringsSettingsNl implements _StringsSettingsEn { - _StringsSettingsNl._(this._root); + _StringsSettingsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Instellingen'; - @override String get language => 'Taal'; - @override String get theme => 'Thema'; - @override String get appearance => 'Uiterlijk'; - @override String get videoPlayback => 'Video afspelen'; - @override String get shufflePlay => 'Willekeurig afspelen'; - @override String get advanced => 'Geavanceerd'; - @override String get useSeasonPostersDescription => 'Toon seizoenposter in plaats van serieposter voor afleveringen'; - @override String get showHeroSectionDescription => 'Toon uitgelichte inhoud carrousel op startscherm'; - @override String get secondsLabel => 'Seconden'; - @override String get minutesLabel => 'Minuten'; - @override String get secondsShort => 's'; - @override String get minutesShort => 'm'; - @override String durationHint({required Object min, required Object max}) => 'Voer duur in (${min}-${max})'; - @override String get systemTheme => 'Systeem'; - @override String get systemThemeDescription => 'Volg systeeminstellingen'; - @override String get lightTheme => 'Licht'; - @override String get darkTheme => 'Donker'; - @override String get libraryDensity => 'Bibliotheek dichtheid'; - @override String get compact => 'Compact'; - @override String get compactDescription => 'Kleinere kaarten, meer items zichtbaar'; - @override String get normal => 'Normaal'; - @override String get normalDescription => 'Standaard grootte'; - @override String get comfortable => 'Comfortabel'; - @override String get comfortableDescription => 'Grotere kaarten, minder items zichtbaar'; - @override String get viewMode => 'Weergavemodus'; - @override String get gridView => 'Raster'; - @override String get gridViewDescription => 'Items weergeven in een rasterindeling'; - @override String get listView => 'Lijst'; - @override String get listViewDescription => 'Items weergeven in een lijstindeling'; - @override String get useSeasonPosters => 'Gebruik seizoenposters'; - @override String get showHeroSection => 'Toon hoofdsectie'; - @override String get hardwareDecoding => 'Hardware decodering'; - @override String get hardwareDecodingDescription => 'Gebruik hardware versnelling indien beschikbaar'; - @override String get bufferSize => 'Buffer grootte'; - @override String bufferSizeMB({required Object size}) => '${size}MB'; - @override String get subtitleStyling => 'Ondertitel opmaak'; - @override String get subtitleStylingDescription => 'Pas ondertitel uiterlijk aan'; - @override String get smallSkipDuration => 'Korte skip duur'; - @override String get largeSkipDuration => 'Lange skip duur'; - @override String secondsUnit({required Object seconds}) => '${seconds} seconden'; - @override String get defaultSleepTimer => 'Standaard slaap timer'; - @override String minutesUnit({required Object minutes}) => 'bij ${minutes} minuten'; - @override String get rememberTrackSelections => 'Onthoud track selecties per serie/film'; - @override String get rememberTrackSelectionsDescription => 'Bewaar automatisch audio- en ondertiteltaalvoorkeuren wanneer je tracks wijzigt tijdens afspelen'; - @override String get unwatchedOnly => 'Alleen ongekeken'; - @override String get unwatchedOnlyDescription => 'Alleen ongekeken afleveringen opnemen in willekeurige wachtrij'; - @override String get shuffleOrderNavigation => 'Willekeurige volgorde navigatie'; - @override String get shuffleOrderNavigationDescription => 'Volgende/vorige knoppen volgen willekeurige volgorde'; - @override String get loopShuffleQueue => 'Herhaal willekeurige wachtrij'; - @override String get loopShuffleQueueDescription => 'Start wachtrij opnieuw bij het einde'; - @override String get videoPlayerControls => 'Videospeler bediening'; - @override String get keyboardShortcuts => 'Toetsenbord sneltoetsen'; - @override String get keyboardShortcutsDescription => 'Pas toetsenbord sneltoetsen aan'; - @override String get debugLogging => 'Debug logging'; - @override String get debugLoggingDescription => 'Schakel gedetailleerde logging in voor probleemoplossing'; - @override String get viewLogs => 'Bekijk logs'; - @override String get viewLogsDescription => 'Bekijk applicatie logs'; - @override String get clearCache => 'Cache wissen'; - @override String get clearCacheDescription => 'Dit wist alle gecachte afbeeldingen en gegevens. De app kan langer duren om inhoud te laden na het wissen van de cache.'; - @override String get clearCacheSuccess => 'Cache succesvol gewist'; - @override String get resetSettings => 'Instellingen resetten'; - @override String get resetSettingsDescription => 'Dit reset alle instellingen naar hun standaard waarden. Deze actie kan niet ongedaan gemaakt worden.'; - @override String get resetSettingsSuccess => 'Instellingen succesvol gereset'; - @override String get shortcutsReset => 'Sneltoetsen gereset naar standaard'; - @override String get about => 'Over'; - @override String get aboutDescription => 'App informatie en licenties'; - @override String get updates => 'Updates'; - @override String get updateAvailable => 'Update beschikbaar'; - @override String get checkForUpdates => 'Controleer op updates'; - @override String get validationErrorEnterNumber => 'Voer een geldig nummer in'; - @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 autoSkip => 'Automatisch Overslaan'; - @override String get autoSkipIntro => 'Intro Automatisch Overslaan'; - @override String get autoSkipIntroDescription => 'Intro-markeringen na enkele seconden automatisch overslaan'; - @override String get autoSkipCredits => 'Credits Automatisch Overslaan'; - @override String get autoSkipCreditsDescription => 'Credits automatisch overslaan en volgende aflevering afspelen'; - @override String get autoSkipDelay => 'Vertraging Automatisch Overslaan'; - @override String autoSkipDelayDescription({required Object seconds}) => '${seconds} seconden wachten voor automatisch overslaan'; + // Translations + @override + String get title => 'Instellingen'; + @override + String get language => 'Taal'; + @override + String get theme => 'Thema'; + @override + String get appearance => 'Uiterlijk'; + @override + String get videoPlayback => 'Video afspelen'; + @override + String get shufflePlay => 'Willekeurig afspelen'; + @override + String get advanced => 'Geavanceerd'; + @override + String get useSeasonPostersDescription => + 'Toon seizoenposter in plaats van serieposter voor afleveringen'; + @override + String get showHeroSectionDescription => + 'Toon uitgelichte inhoud carrousel op startscherm'; + @override + String get secondsLabel => 'Seconden'; + @override + String get minutesLabel => 'Minuten'; + @override + String get secondsShort => 's'; + @override + String get minutesShort => 'm'; + @override + String durationHint({required Object min, required Object max}) => + 'Voer duur in (${min}-${max})'; + @override + String get systemTheme => 'Systeem'; + @override + String get systemThemeDescription => 'Volg systeeminstellingen'; + @override + String get lightTheme => 'Licht'; + @override + String get darkTheme => 'Donker'; + @override + String get libraryDensity => 'Bibliotheek dichtheid'; + @override + String get compact => 'Compact'; + @override + String get compactDescription => 'Kleinere kaarten, meer items zichtbaar'; + @override + String get normal => 'Normaal'; + @override + String get normalDescription => 'Standaard grootte'; + @override + String get comfortable => 'Comfortabel'; + @override + String get comfortableDescription => + 'Grotere kaarten, minder items zichtbaar'; + @override + String get viewMode => 'Weergavemodus'; + @override + String get gridView => 'Raster'; + @override + String get gridViewDescription => 'Items weergeven in een rasterindeling'; + @override + String get listView => 'Lijst'; + @override + String get listViewDescription => 'Items weergeven in een lijstindeling'; + @override + String get useSeasonPosters => 'Gebruik seizoenposters'; + @override + String get showHeroSection => 'Toon hoofdsectie'; + @override + String get hardwareDecoding => 'Hardware decodering'; + @override + String get hardwareDecodingDescription => + 'Gebruik hardware versnelling indien beschikbaar'; + @override + String get bufferSize => 'Buffer grootte'; + @override + String bufferSizeMB({required Object size}) => '${size}MB'; + @override + String get subtitleStyling => 'Ondertitel opmaak'; + @override + String get subtitleStylingDescription => 'Pas ondertitel uiterlijk aan'; + @override + String get smallSkipDuration => 'Korte skip duur'; + @override + String get largeSkipDuration => 'Lange skip duur'; + @override + String secondsUnit({required Object seconds}) => '${seconds} seconden'; + @override + String get defaultSleepTimer => 'Standaard slaap timer'; + @override + String minutesUnit({required Object minutes}) => 'bij ${minutes} minuten'; + @override + String get rememberTrackSelections => + 'Onthoud track selecties per serie/film'; + @override + String get rememberTrackSelectionsDescription => + 'Bewaar automatisch audio- en ondertiteltaalvoorkeuren wanneer je tracks wijzigt tijdens afspelen'; + @override + String get unwatchedOnly => 'Alleen ongekeken'; + @override + String get unwatchedOnlyDescription => + 'Alleen ongekeken afleveringen opnemen in willekeurige wachtrij'; + @override + String get shuffleOrderNavigation => 'Willekeurige volgorde navigatie'; + @override + String get shuffleOrderNavigationDescription => + 'Volgende/vorige knoppen volgen willekeurige volgorde'; + @override + String get loopShuffleQueue => 'Herhaal willekeurige wachtrij'; + @override + String get loopShuffleQueueDescription => + 'Start wachtrij opnieuw bij het einde'; + @override + String get videoPlayerControls => 'Videospeler bediening'; + @override + String get keyboardShortcuts => 'Toetsenbord sneltoetsen'; + @override + String get keyboardShortcutsDescription => 'Pas toetsenbord sneltoetsen aan'; + @override + String get debugLogging => 'Debug logging'; + @override + String get debugLoggingDescription => + 'Schakel gedetailleerde logging in voor probleemoplossing'; + @override + String get viewLogs => 'Bekijk logs'; + @override + String get viewLogsDescription => 'Bekijk applicatie logs'; + @override + String get clearCache => 'Cache wissen'; + @override + String get clearCacheDescription => + 'Dit wist alle gecachte afbeeldingen en gegevens. De app kan langer duren om inhoud te laden na het wissen van de cache.'; + @override + String get clearCacheSuccess => 'Cache succesvol gewist'; + @override + String get resetSettings => 'Instellingen resetten'; + @override + String get resetSettingsDescription => + 'Dit reset alle instellingen naar hun standaard waarden. Deze actie kan niet ongedaan gemaakt worden.'; + @override + String get resetSettingsSuccess => 'Instellingen succesvol gereset'; + @override + String get shortcutsReset => 'Sneltoetsen gereset naar standaard'; + @override + String get about => 'Over'; + @override + String get aboutDescription => 'App informatie en licenties'; + @override + String get updates => 'Updates'; + @override + String get updateAvailable => 'Update beschikbaar'; + @override + String get checkForUpdates => 'Controleer op updates'; + @override + String get validationErrorEnterNumber => 'Voer een geldig nummer in'; + @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 autoSkip => 'Automatisch Overslaan'; + @override + String get autoSkipIntro => 'Intro Automatisch Overslaan'; + @override + String get autoSkipIntroDescription => + 'Intro-markeringen na enkele seconden automatisch overslaan'; + @override + String get autoSkipCredits => 'Credits Automatisch Overslaan'; + @override + String get autoSkipCreditsDescription => + 'Credits automatisch overslaan en volgende aflevering afspelen'; + @override + String get autoSkipDelay => 'Vertraging Automatisch Overslaan'; + @override + String autoSkipDelayDescription({required Object seconds}) => + '${seconds} seconden wachten voor automatisch overslaan'; + @override + String get downloads => 'Downloads'; + @override + String get downloadLocationDescription => + 'Kies waar gedownloade content wordt opgeslagen'; + @override + String get downloadLocationDefault => 'Standaard (App-opslag)'; + @override + String get downloadLocationCustom => 'Aangepaste Locatie'; + @override + String get selectFolder => 'Selecteer Map'; + @override + String get resetToDefault => 'Herstel naar Standaard'; + @override + String currentPath({required Object path}) => 'Huidig: ${path}'; + @override + String get downloadLocationNotSupported => + 'Aangepaste downloadlocatie wordt niet ondersteund op iOS'; + @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 downloadOnWifiOnly => 'Alleen via WiFi downloaden'; + @override + String get downloadOnWifiOnlyDescription => + 'Voorkom downloads bij gebruik van mobiele data'; + @override + String get cellularDownloadBlocked => + 'Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.'; } // Path: search class _StringsSearchNl implements _StringsSearchEn { - _StringsSearchNl._(this._root); + _StringsSearchNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get hint => 'Zoek films, series, muziek...'; - @override String get tryDifferentTerm => 'Probeer een andere zoekterm'; - @override String get searchYourMedia => 'Zoek in je media'; - @override String get enterTitleActorOrKeyword => 'Voer een titel, acteur of trefwoord in'; + // Translations + @override + String get hint => 'Zoek films, series, muziek...'; + @override + String get tryDifferentTerm => 'Probeer een andere zoekterm'; + @override + String get searchYourMedia => 'Zoek in je media'; + @override + String get enterTitleActorOrKeyword => + 'Voer een titel, acteur of trefwoord in'; } // Path: hotkeys class _StringsHotkeysNl implements _StringsHotkeysEn { - _StringsHotkeysNl._(this._root); + _StringsHotkeysNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String setShortcutFor({required Object actionName}) => 'Stel sneltoets in voor ${actionName}'; - @override String get clearShortcut => 'Wis sneltoets'; + // Translations + @override + String setShortcutFor({required Object actionName}) => + 'Stel sneltoets in voor ${actionName}'; + @override + String get clearShortcut => 'Wis sneltoets'; } // Path: pinEntry class _StringsPinEntryNl implements _StringsPinEntryEn { - _StringsPinEntryNl._(this._root); + _StringsPinEntryNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get enterPin => 'Voer PIN in'; - @override String get showPin => 'Toon PIN'; - @override String get hidePin => 'Verberg PIN'; + // Translations + @override + String get enterPin => 'Voer PIN in'; + @override + String get showPin => 'Toon PIN'; + @override + String get hidePin => 'Verberg PIN'; } // Path: fileInfo class _StringsFileInfoNl implements _StringsFileInfoEn { - _StringsFileInfoNl._(this._root); + _StringsFileInfoNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Bestand info'; - @override String get video => 'Video'; - @override String get audio => 'Audio'; - @override String get file => 'Bestand'; - @override String get advanced => 'Geavanceerd'; - @override String get codec => 'Codec'; - @override String get resolution => 'Resolutie'; - @override String get bitrate => 'Bitrate'; - @override String get frameRate => 'Frame rate'; - @override String get aspectRatio => 'Beeldverhouding'; - @override String get profile => 'Profiel'; - @override String get bitDepth => 'Bit diepte'; - @override String get colorSpace => 'Kleurruimte'; - @override String get colorRange => 'Kleurbereik'; - @override String get colorPrimaries => 'Kleurprimaires'; - @override String get chromaSubsampling => 'Chroma subsampling'; - @override String get channels => 'Kanalen'; - @override String get path => 'Pad'; - @override String get size => 'Grootte'; - @override String get container => 'Container'; - @override String get duration => 'Duur'; - @override String get optimizedForStreaming => 'Geoptimaliseerd voor streaming'; - @override String get has64bitOffsets => '64-bit Offsets'; + // Translations + @override + String get title => 'Bestand info'; + @override + String get video => 'Video'; + @override + String get audio => 'Audio'; + @override + String get file => 'Bestand'; + @override + String get advanced => 'Geavanceerd'; + @override + String get codec => 'Codec'; + @override + String get resolution => 'Resolutie'; + @override + String get bitrate => 'Bitrate'; + @override + String get frameRate => 'Frame rate'; + @override + String get aspectRatio => 'Beeldverhouding'; + @override + String get profile => 'Profiel'; + @override + String get bitDepth => 'Bit diepte'; + @override + String get colorSpace => 'Kleurruimte'; + @override + String get colorRange => 'Kleurbereik'; + @override + String get colorPrimaries => 'Kleurprimaires'; + @override + String get chromaSubsampling => 'Chroma subsampling'; + @override + String get channels => 'Kanalen'; + @override + String get path => 'Pad'; + @override + String get size => 'Grootte'; + @override + String get container => 'Container'; + @override + String get duration => 'Duur'; + @override + String get optimizedForStreaming => 'Geoptimaliseerd voor streaming'; + @override + String get has64bitOffsets => '64-bit Offsets'; } // Path: mediaMenu class _StringsMediaMenuNl implements _StringsMediaMenuEn { - _StringsMediaMenuNl._(this._root); + _StringsMediaMenuNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get markAsWatched => 'Markeer als gekeken'; - @override String get markAsUnwatched => 'Markeer als ongekeken'; - @override String get removeFromContinueWatching => 'Verwijder uit Doorgaan met kijken'; - @override String get goToSeries => 'Ga naar serie'; - @override String get goToSeason => 'Ga naar seizoen'; - @override String get shufflePlay => 'Willekeurig afspelen'; - @override String get fileInfo => 'Bestand info'; + // Translations + @override + String get markAsWatched => 'Markeer als gekeken'; + @override + String get markAsUnwatched => 'Markeer als ongekeken'; + @override + String get removeFromContinueWatching => 'Verwijder uit Doorgaan met kijken'; + @override + String get goToSeries => 'Ga naar serie'; + @override + String get goToSeason => 'Ga naar seizoen'; + @override + String get shufflePlay => 'Willekeurig afspelen'; + @override + String get fileInfo => 'Bestand info'; } // Path: accessibility class _StringsAccessibilityNl implements _StringsAccessibilityEn { - _StringsAccessibilityNl._(this._root); + _StringsAccessibilityNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String mediaCardMovie({required Object title}) => '${title}, film'; - @override String mediaCardShow({required Object title}) => '${title}, TV-serie'; - @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - @override String get mediaCardWatched => 'bekeken'; - @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} procent bekeken'; - @override String get mediaCardUnwatched => 'niet bekeken'; - @override String get tapToPlay => 'Tik om af te spelen'; + // Translations + @override + String mediaCardMovie({required Object title}) => '${title}, film'; + @override + String mediaCardShow({required Object title}) => '${title}, TV-serie'; + @override + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + @override + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + @override + String get mediaCardWatched => 'bekeken'; + @override + String mediaCardPartiallyWatched({required Object percent}) => + '${percent} procent bekeken'; + @override + String get mediaCardUnwatched => 'niet bekeken'; + @override + String get tapToPlay => 'Tik om af te spelen'; } // Path: tooltips class _StringsTooltipsNl implements _StringsTooltipsEn { - _StringsTooltipsNl._(this._root); + _StringsTooltipsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get shufflePlay => 'Willekeurig afspelen'; - @override String get markAsWatched => 'Markeer als gekeken'; - @override String get markAsUnwatched => 'Markeer als ongekeken'; + // Translations + @override + String get shufflePlay => 'Willekeurig afspelen'; + @override + String get markAsWatched => 'Markeer als gekeken'; + @override + String get markAsUnwatched => 'Markeer als ongekeken'; } // Path: videoControls class _StringsVideoControlsNl implements _StringsVideoControlsEn { - _StringsVideoControlsNl._(this._root); + _StringsVideoControlsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get audioLabel => 'Audio'; - @override String get subtitlesLabel => 'Ondertitels'; - @override String get resetToZero => 'Reset naar 0ms'; - @override String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - @override String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - @override String playsLater({required Object label}) => '${label} speelt later af'; - @override String playsEarlier({required Object label}) => '${label} speelt eerder af'; - @override String get noOffset => 'Geen offset'; - @override String get letterbox => 'Letterbox'; - @override String get fillScreen => 'Vul scherm'; - @override String get stretch => 'Uitrekken'; - @override String get lockRotation => 'Vergrendel rotatie'; - @override String get unlockRotation => 'Ontgrendel rotatie'; - @override String get sleepTimer => 'Slaaptimer'; - @override String get timerActive => 'Timer actief'; - @override String playbackWillPauseIn({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}'; - @override String get sleepTimerCompleted => 'Slaaptimer voltooid - afspelen gepauzeerd'; - @override String get playButton => 'Afspelen'; - @override String get pauseButton => 'Pauzeren'; - @override String seekBackwardButton({required Object seconds}) => 'Terugspoelen ${seconds} seconden'; - @override String seekForwardButton({required Object seconds}) => 'Vooruitspoelen ${seconds} seconden'; - @override String get previousButton => 'Vorige aflevering'; - @override String get nextButton => 'Volgende aflevering'; - @override String get previousChapterButton => 'Vorig hoofdstuk'; - @override String get nextChapterButton => 'Volgend hoofdstuk'; - @override String get volumeButton => 'Volume'; - @override String get muteButton => 'Dempen'; - @override String get unmuteButton => 'Dempen opheffen'; - @override String get settingsButton => 'Video-instellingen'; - @override String get audioTrackButton => 'Audiosporen'; - @override String get subtitlesButton => 'Ondertitels'; - @override String get chaptersButton => 'Hoofdstukken'; - @override String get versionsButton => 'Videoversies'; - @override String get aspectRatioButton => 'Beeldverhouding'; - @override String get fullscreenButton => 'Volledig scherm activeren'; - @override String get exitFullscreenButton => 'Volledig scherm verlaten'; - @override String get rotationLockButton => 'Rotatievergrendeling'; - @override String get timelineSlider => 'Videotijdlijn'; - @override String get volumeSlider => 'Volumeniveau'; - @override String get backButton => 'Terug'; + // Translations + @override + String get audioLabel => 'Audio'; + @override + String get subtitlesLabel => 'Ondertitels'; + @override + String get resetToZero => 'Reset naar 0ms'; + @override + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + @override + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + @override + String playsLater({required Object label}) => '${label} speelt later af'; + @override + String playsEarlier({required Object label}) => '${label} speelt eerder af'; + @override + String get noOffset => 'Geen offset'; + @override + String get letterbox => 'Letterbox'; + @override + String get fillScreen => 'Vul scherm'; + @override + String get stretch => 'Uitrekken'; + @override + String get lockRotation => 'Vergrendel rotatie'; + @override + String get unlockRotation => 'Ontgrendel rotatie'; + @override + String get sleepTimer => 'Slaaptimer'; + @override + String get timerActive => 'Timer actief'; + @override + String playbackWillPauseIn({required Object duration}) => + 'Afspelen wordt gepauzeerd over ${duration}'; + @override + String get sleepTimerCompleted => 'Slaaptimer voltooid - afspelen gepauzeerd'; + @override + String get playButton => 'Afspelen'; + @override + String get pauseButton => 'Pauzeren'; + @override + String seekBackwardButton({required Object seconds}) => + 'Terugspoelen ${seconds} seconden'; + @override + String seekForwardButton({required Object seconds}) => + 'Vooruitspoelen ${seconds} seconden'; + @override + String get previousButton => 'Vorige aflevering'; + @override + String get nextButton => 'Volgende aflevering'; + @override + String get previousChapterButton => 'Vorig hoofdstuk'; + @override + String get nextChapterButton => 'Volgend hoofdstuk'; + @override + String get volumeButton => 'Volume'; + @override + String get muteButton => 'Dempen'; + @override + String get unmuteButton => 'Dempen opheffen'; + @override + String get settingsButton => 'Video-instellingen'; + @override + String get audioTrackButton => 'Audiosporen'; + @override + String get subtitlesButton => 'Ondertitels'; + @override + String get chaptersButton => 'Hoofdstukken'; + @override + String get versionsButton => 'Videoversies'; + @override + String get aspectRatioButton => 'Beeldverhouding'; + @override + String get fullscreenButton => 'Volledig scherm activeren'; + @override + String get exitFullscreenButton => 'Volledig scherm verlaten'; + @override + String get rotationLockButton => 'Rotatievergrendeling'; + @override + String get timelineSlider => 'Videotijdlijn'; + @override + String get volumeSlider => 'Volumeniveau'; + @override + String get backButton => 'Terug'; } // Path: userStatus class _StringsUserStatusNl implements _StringsUserStatusEn { - _StringsUserStatusNl._(this._root); + _StringsUserStatusNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get admin => 'Beheerder'; - @override String get restricted => 'Beperkt'; - @override String get protected => 'Beschermd'; - @override String get current => 'HUIDIG'; + // Translations + @override + String get admin => 'Beheerder'; + @override + String get restricted => 'Beperkt'; + @override + String get protected => 'Beschermd'; + @override + String get current => 'HUIDIG'; } // Path: messages class _StringsMessagesNl implements _StringsMessagesEn { - _StringsMessagesNl._(this._root); + _StringsMessagesNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get markedAsWatched => 'Gemarkeerd als gekeken'; - @override String get markedAsUnwatched => 'Gemarkeerd als ongekeken'; - @override String get removedFromContinueWatching => 'Verwijderd uit Doorgaan met kijken'; - @override String errorLoading({required Object error}) => 'Fout: ${error}'; - @override String get fileInfoNotAvailable => 'Bestand informatie niet beschikbaar'; - @override String errorLoadingFileInfo({required Object error}) => 'Fout bij laden bestand info: ${error}'; - @override String get errorLoadingSeries => 'Fout bij laden serie'; - @override String get errorLoadingSeason => 'Fout bij laden seizoen'; - @override String get musicNotSupported => 'Muziek afspelen wordt nog niet ondersteund'; - @override String get logsCleared => 'Logs gewist'; - @override String get logsCopied => 'Logs gekopieerd naar klembord'; - @override String get noLogsAvailable => 'Geen logs beschikbaar'; - @override String libraryScanning({required Object title}) => 'Scannen "${title}"...'; - @override String libraryScanStarted({required Object title}) => 'Bibliotheek scan gestart voor "${title}"'; - @override String libraryScanFailed({required Object error}) => 'Kon bibliotheek niet scannen: ${error}'; - @override String metadataRefreshing({required Object title}) => 'Metadata vernieuwen voor "${title}"...'; - @override String metadataRefreshStarted({required Object title}) => 'Metadata vernieuwen gestart voor "${title}"'; - @override String metadataRefreshFailed({required Object error}) => 'Kon metadata niet vernieuwen: ${error}'; - @override String get noPlexToken => 'Geen Plex token gevonden. Log opnieuw in.'; - @override String get logoutConfirm => 'Weet je zeker dat je wilt uitloggen?'; - @override String get noSeasonsFound => 'Geen seizoenen gevonden'; - @override String get noEpisodesFound => 'Geen afleveringen gevonden in eerste seizoen'; - @override String get noEpisodesFoundGeneral => 'Geen afleveringen gevonden'; - @override String get noResultsFound => 'Geen resultaten gevonden'; - @override String sleepTimerSet({required Object label}) => 'Slaap timer ingesteld voor ${label}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Kon niet wisselen naar ${displayName}'; - @override String get noItemsAvailable => 'Geen items beschikbaar'; - @override String get failedToCreatePlayQueue => 'Kan afspeelwachtrij niet maken'; - @override String get failedToCreatePlayQueueNoItems => 'Kan afspeelwachtrij niet maken - geen items'; - @override String failedPlayback({required Object action, required Object error}) => 'Afspelen van ${action} mislukt: ${error}'; + // Translations + @override + String get markedAsWatched => 'Gemarkeerd als gekeken'; + @override + String get markedAsUnwatched => 'Gemarkeerd als ongekeken'; + @override + String get markedAsWatchedOffline => + 'Gemarkeerd als gekeken (sync wanneer online)'; + @override + String get markedAsUnwatchedOffline => + 'Gemarkeerd als ongekeken (sync wanneer online)'; + @override + String get removedFromContinueWatching => + 'Verwijderd uit Doorgaan met kijken'; + @override + String errorLoading({required Object error}) => 'Fout: ${error}'; + @override + String get fileInfoNotAvailable => 'Bestand informatie niet beschikbaar'; + @override + String errorLoadingFileInfo({required Object error}) => + 'Fout bij laden bestand info: ${error}'; + @override + String get errorLoadingSeries => 'Fout bij laden serie'; + @override + String get errorLoadingSeason => 'Fout bij laden seizoen'; + @override + String get musicNotSupported => 'Muziek afspelen wordt nog niet ondersteund'; + @override + String get logsCleared => 'Logs gewist'; + @override + String get logsCopied => 'Logs gekopieerd naar klembord'; + @override + String get noLogsAvailable => 'Geen logs beschikbaar'; + @override + String libraryScanning({required Object title}) => 'Scannen "${title}"...'; + @override + String libraryScanStarted({required Object title}) => + 'Bibliotheek scan gestart voor "${title}"'; + @override + String libraryScanFailed({required Object error}) => + 'Kon bibliotheek niet scannen: ${error}'; + @override + String metadataRefreshing({required Object title}) => + 'Metadata vernieuwen voor "${title}"...'; + @override + String metadataRefreshStarted({required Object title}) => + 'Metadata vernieuwen gestart voor "${title}"'; + @override + String metadataRefreshFailed({required Object error}) => + 'Kon metadata niet vernieuwen: ${error}'; + @override + String get noPlexToken => 'Geen Plex token gevonden. Log opnieuw in.'; + @override + String get logoutConfirm => 'Weet je zeker dat je wilt uitloggen?'; + @override + String get noSeasonsFound => 'Geen seizoenen gevonden'; + @override + String get noEpisodesFound => 'Geen afleveringen gevonden in eerste seizoen'; + @override + String get noEpisodesFoundGeneral => 'Geen afleveringen gevonden'; + @override + String get noResultsFound => 'Geen resultaten gevonden'; + @override + String sleepTimerSet({required Object label}) => + 'Slaap timer ingesteld voor ${label}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Kon niet wisselen naar ${displayName}'; + @override + String get noItemsAvailable => 'Geen items beschikbaar'; + @override + String get failedToCreatePlayQueue => 'Kan afspeelwachtrij niet maken'; + @override + String get failedToCreatePlayQueueNoItems => + 'Kan afspeelwachtrij niet maken - geen items'; + @override + String failedPlayback({required Object action, required Object error}) => + 'Afspelen van ${action} mislukt: ${error}'; + @override + String get youAreOffline => 'Je bent offline'; + @override + String get offlineFeatureUnavailable => + 'Deze functie vereist een internetverbinding. Je downloads en instellingen zijn nog steeds beschikbaar.'; } // Path: profile class _StringsProfileNl implements _StringsProfileEn { - _StringsProfileNl._(this._root); + _StringsProfileNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get noUsersAvailable => 'Geen gebruikers beschikbaar'; + // Translations + @override + String get noUsersAvailable => 'Geen gebruikers beschikbaar'; } // Path: subtitlingStyling class _StringsSubtitlingStylingNl implements _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingNl._(this._root); + _StringsSubtitlingStylingNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get stylingOptions => 'Opmaak opties'; - @override String get fontSize => 'Lettergrootte'; - @override String get textColor => 'Tekstkleur'; - @override String get borderSize => 'Rand grootte'; - @override String get borderColor => 'Randkleur'; - @override String get backgroundOpacity => 'Achtergrond transparantie'; - @override String get backgroundColor => 'Achtergrondkleur'; + // Translations + @override + String get stylingOptions => 'Opmaak opties'; + @override + String get fontSize => 'Lettergrootte'; + @override + String get textColor => 'Tekstkleur'; + @override + String get borderSize => 'Rand grootte'; + @override + String get borderColor => 'Randkleur'; + @override + String get backgroundOpacity => 'Achtergrond transparantie'; + @override + String get backgroundColor => 'Achtergrondkleur'; } // Path: dialog class _StringsDialogNl implements _StringsDialogEn { - _StringsDialogNl._(this._root); + _StringsDialogNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get confirmAction => 'Bevestig actie'; - @override String get areYouSure => 'Weet je zeker dat je deze actie wilt uitvoeren?'; - @override String get cancel => 'Annuleren'; - @override String get playNow => 'Nu afspelen'; + // Translations + @override + String get confirmAction => 'Bevestig actie'; + @override + String get areYouSure => 'Weet je zeker dat je deze actie wilt uitvoeren?'; + @override + String get cancel => 'Annuleren'; + @override + String get playNow => 'Nu afspelen'; } // Path: discover class _StringsDiscoverNl implements _StringsDiscoverEn { - _StringsDiscoverNl._(this._root); + _StringsDiscoverNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Ontdekken'; - @override String get switchProfile => 'Wissel van profiel'; - @override String get switchServer => 'Wissel van server'; - @override String get logout => 'Uitloggen'; - @override String get noContentAvailable => 'Geen inhoud beschikbaar'; - @override String get addMediaToLibraries => 'Voeg wat media toe aan je bibliotheken'; - @override String get continueWatching => 'Verder kijken'; - @override String get recentlyAdded => 'Recent toegevoegd'; - @override String get play => 'Afspelen'; - @override String get resume => 'Hervatten'; - @override String playEpisode({required Object season, required Object episode}) => 'Speel S${season}, E${episode}'; - @override String resumeEpisode({required Object season, required Object episode}) => 'Hervat S${season}, E${episode}'; - @override String get pause => 'Pauzeren'; - @override String get overview => 'Overzicht'; - @override String get cast => 'Acteurs'; - @override String get seasons => 'Seizoenen'; - @override String get studio => 'Studio'; - @override String get rating => 'Leeftijd'; - @override String get watched => 'Bekeken'; - @override String episodeCount({required Object count}) => '${count} afleveringen'; - @override String watchedProgress({required Object watched, required Object total}) => '${watched}/${total} gekeken'; - @override String get movie => 'Film'; - @override String get tvShow => 'TV Serie'; - @override String minutesLeft({required Object minutes}) => '${minutes} min over'; + // Translations + @override + String get title => 'Ontdekken'; + @override + String get switchProfile => 'Wissel van profiel'; + @override + String get switchServer => 'Wissel van server'; + @override + String get logout => 'Uitloggen'; + @override + String get noContentAvailable => 'Geen inhoud beschikbaar'; + @override + String get addMediaToLibraries => 'Voeg wat media toe aan je bibliotheken'; + @override + String get continueWatching => 'Verder kijken'; + @override + String get recentlyAdded => 'Recent toegevoegd'; + @override + String get play => 'Afspelen'; + @override + String get resume => 'Hervatten'; + @override + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String get pause => 'Pauzeren'; + @override + String get overview => 'Overzicht'; + @override + String get cast => 'Acteurs'; + @override + String get seasons => 'Seizoenen'; + @override + String get studio => 'Studio'; + @override + String get rating => 'Leeftijd'; + @override + String get watched => 'Bekeken'; + @override + String episodeCount({required Object count}) => '${count} afleveringen'; + @override + String watchedProgress({required Object watched, required Object total}) => + '${watched}/${total} gekeken'; + @override + String get movie => 'Film'; + @override + String get tvShow => 'TV Serie'; + @override + String minutesLeft({required Object minutes}) => '${minutes} min over'; } // Path: errors class _StringsErrorsNl implements _StringsErrorsEn { - _StringsErrorsNl._(this._root); + _StringsErrorsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String searchFailed({required Object error}) => 'Zoeken mislukt: ${error}'; - @override String connectionTimeout({required Object context}) => 'Verbinding time-out tijdens laden ${context}'; - @override String get connectionFailed => 'Kan geen verbinding maken met Plex server'; - @override String failedToLoad({required Object context, required Object error}) => 'Kon ${context} niet laden: ${error}'; - @override String get noClientAvailable => 'Geen client beschikbaar'; - @override String authenticationFailed({required Object error}) => 'Authenticatie mislukt: ${error}'; - @override String get couldNotLaunchUrl => 'Kon auth URL niet openen'; - @override String get pleaseEnterToken => 'Voer een token in'; - @override String get invalidToken => 'Ongeldig token'; - @override String failedToVerifyToken({required Object error}) => 'Kon token niet verifiëren: ${error}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Kon niet wisselen naar ${displayName}'; - @override String get connectionFailedGeneric => 'Verbinding mislukt'; + // Translations + @override + String searchFailed({required Object error}) => 'Zoeken mislukt: ${error}'; + @override + String connectionTimeout({required Object context}) => + 'Verbinding time-out tijdens laden ${context}'; + @override + String get connectionFailed => 'Kan geen verbinding maken met Plex server'; + @override + String failedToLoad({required Object context, required Object error}) => + 'Kon ${context} niet laden: ${error}'; + @override + String get noClientAvailable => 'Geen client beschikbaar'; + @override + String authenticationFailed({required Object error}) => + 'Authenticatie mislukt: ${error}'; + @override + String get couldNotLaunchUrl => 'Kon auth URL niet openen'; + @override + String get pleaseEnterToken => 'Voer een token in'; + @override + String get invalidToken => 'Ongeldig token'; + @override + String failedToVerifyToken({required Object error}) => + 'Kon token niet verifiëren: ${error}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Kon niet wisselen naar ${displayName}'; + @override + String get connectionFailedGeneric => 'Verbinding mislukt'; } // Path: libraries class _StringsLibrariesNl implements _StringsLibrariesEn { - _StringsLibrariesNl._(this._root); + _StringsLibrariesNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Bibliotheken'; - @override String get scanLibraryFiles => 'Scan bibliotheek bestanden'; - @override String get scanLibrary => 'Scan bibliotheek'; - @override String get analyze => 'Analyseren'; - @override String get analyzeLibrary => 'Analyseer bibliotheek'; - @override String get refreshMetadata => 'Vernieuw metadata'; - @override String get emptyTrash => 'Prullenbak legen'; - @override String emptyingTrash({required Object title}) => 'Prullenbak legen voor "${title}"...'; - @override String trashEmptied({required Object title}) => 'Prullenbak geleegd voor "${title}"'; - @override String failedToEmptyTrash({required Object error}) => 'Kon prullenbak niet legen: ${error}'; - @override String analyzing({required Object title}) => 'Analyseren "${title}"...'; - @override String analysisStarted({required Object title}) => 'Analyse gestart voor "${title}"'; - @override String failedToAnalyze({required Object error}) => 'Kon bibliotheek niet analyseren: ${error}'; - @override String get noLibrariesFound => 'Geen bibliotheken gevonden'; - @override String get thisLibraryIsEmpty => 'Deze bibliotheek is leeg'; - @override String get all => 'Alles'; - @override String get clearAll => 'Alles wissen'; - @override String scanLibraryConfirm({required Object title}) => 'Weet je zeker dat je "${title}" wilt scannen?'; - @override String analyzeLibraryConfirm({required Object title}) => 'Weet je zeker dat je "${title}" wilt analyseren?'; - @override String refreshMetadataConfirm({required Object title}) => 'Weet je zeker dat je metadata wilt vernieuwen voor "${title}"?'; - @override String emptyTrashConfirm({required Object title}) => 'Weet je zeker dat je de prullenbak wilt legen voor "${title}"?'; - @override String get manageLibraries => 'Beheer bibliotheken'; - @override String get sort => 'Sorteren'; - @override String get sortBy => 'Sorteer op'; - @override String get filters => 'Filters'; - @override String loadingLibraryWithCount({required Object count}) => 'Bibliotheek laden... (${count} items geladen)'; - @override String get confirmActionMessage => 'Weet je zeker dat je deze actie wilt uitvoeren?'; - @override String get showLibrary => 'Toon bibliotheek'; - @override String get hideLibrary => 'Verberg bibliotheek'; - @override String get libraryOptions => 'Bibliotheek opties'; - @override String get content => 'bibliotheekinhoud'; - @override String get selectLibrary => 'Bibliotheek kiezen'; - @override String filtersWithCount({required Object count}) => 'Filters (${count})'; - @override String get noRecommendations => 'Geen aanbevelingen beschikbaar'; - @override String get noCollections => 'Geen collecties in deze bibliotheek'; - @override String get noFoldersFound => 'Geen mappen gevonden'; - @override String get folders => 'mappen'; - @override late final _StringsLibrariesTabsNl tabs = _StringsLibrariesTabsNl._(_root); - @override late final _StringsLibrariesGroupingsNl groupings = _StringsLibrariesGroupingsNl._(_root); + // Translations + @override + String get title => 'Bibliotheken'; + @override + String get scanLibraryFiles => 'Scan bibliotheek bestanden'; + @override + String get scanLibrary => 'Scan bibliotheek'; + @override + String get analyze => 'Analyseren'; + @override + String get analyzeLibrary => 'Analyseer bibliotheek'; + @override + String get refreshMetadata => 'Vernieuw metadata'; + @override + String get emptyTrash => 'Prullenbak legen'; + @override + String emptyingTrash({required Object title}) => + 'Prullenbak legen voor "${title}"...'; + @override + String trashEmptied({required Object title}) => + 'Prullenbak geleegd voor "${title}"'; + @override + String failedToEmptyTrash({required Object error}) => + 'Kon prullenbak niet legen: ${error}'; + @override + String analyzing({required Object title}) => 'Analyseren "${title}"...'; + @override + String analysisStarted({required Object title}) => + 'Analyse gestart voor "${title}"'; + @override + String failedToAnalyze({required Object error}) => + 'Kon bibliotheek niet analyseren: ${error}'; + @override + String get noLibrariesFound => 'Geen bibliotheken gevonden'; + @override + String get thisLibraryIsEmpty => 'Deze bibliotheek is leeg'; + @override + String get all => 'Alles'; + @override + String get clearAll => 'Alles wissen'; + @override + String scanLibraryConfirm({required Object title}) => + 'Weet je zeker dat je "${title}" wilt scannen?'; + @override + String analyzeLibraryConfirm({required Object title}) => + 'Weet je zeker dat je "${title}" wilt analyseren?'; + @override + String refreshMetadataConfirm({required Object title}) => + 'Weet je zeker dat je metadata wilt vernieuwen voor "${title}"?'; + @override + String emptyTrashConfirm({required Object title}) => + 'Weet je zeker dat je de prullenbak wilt legen voor "${title}"?'; + @override + String get manageLibraries => 'Beheer bibliotheken'; + @override + String get sort => 'Sorteren'; + @override + String get sortBy => 'Sorteer op'; + @override + String get filters => 'Filters'; + @override + String loadingLibraryWithCount({required Object count}) => + 'Bibliotheek laden... (${count} items geladen)'; + @override + String get confirmActionMessage => + 'Weet je zeker dat je deze actie wilt uitvoeren?'; + @override + String get showLibrary => 'Toon bibliotheek'; + @override + String get hideLibrary => 'Verberg bibliotheek'; + @override + String get libraryOptions => 'Bibliotheek opties'; + @override + String get content => 'bibliotheekinhoud'; + @override + String get selectLibrary => 'Bibliotheek kiezen'; + @override + String filtersWithCount({required Object count}) => 'Filters (${count})'; + @override + String get noRecommendations => 'Geen aanbevelingen beschikbaar'; + @override + String get noCollections => 'Geen collecties in deze bibliotheek'; + @override + String get noFoldersFound => 'Geen mappen gevonden'; + @override + String get folders => 'mappen'; + @override + late final _StringsLibrariesTabsNl tabs = _StringsLibrariesTabsNl._(_root); + @override + late final _StringsLibrariesGroupingsNl groupings = + _StringsLibrariesGroupingsNl._(_root); } // Path: about class _StringsAboutNl implements _StringsAboutEn { - _StringsAboutNl._(this._root); + _StringsAboutNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Over'; - @override String get openSourceLicenses => 'Open Source licenties'; - @override String versionLabel({required Object version}) => 'Versie ${version}'; - @override String get appDescription => 'Een mooie Plex client voor Flutter'; - @override String get viewLicensesDescription => 'Bekijk licenties van third-party bibliotheken'; + // Translations + @override + String get title => 'Over'; + @override + String get openSourceLicenses => 'Open Source licenties'; + @override + String versionLabel({required Object version}) => 'Versie ${version}'; + @override + String get appDescription => 'Een mooie Plex client voor Flutter'; + @override + String get viewLicensesDescription => + 'Bekijk licenties van third-party bibliotheken'; } // Path: serverSelection class _StringsServerSelectionNl implements _StringsServerSelectionEn { - _StringsServerSelectionNl._(this._root); + _StringsServerSelectionNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get connectingToServer => 'Verbinden met server...'; - @override String get connectingToAllServers => 'Verbinden met alle servers...'; - @override String get connectToAllServers => 'Verbind met alle servers'; - @override String get allServerConnectionsFailed => 'Kon niet verbinden met servers. Controleer je netwerk en probeer opnieuw.'; - @override String foundServers({required Object count}) => '${count} server(s) gevonden'; - @override String get orSelectIndividually => 'Of selecteer een server individueel:'; - @override String get serverDebugCopied => 'Server debug gegevens gekopieerd naar klembord'; - @override String get copyDebugData => 'Kopieer debug gegevens'; - @override String get noServersFound => 'Geen servers gevonden'; - @override String noServersFoundForAccount({required Object username, required Object email}) => 'Geen servers gevonden voor ${username} (${email})'; - @override String malformedServerData({required Object count}) => '${count} server(s) gevonden met verkeerde data. Geen geldige servers beschikbaar.'; - @override String get incompleteServerInfo => 'Sommige servers hebben incomplete informatie en zijn overgeslagen. Controleer je Plex.tv account.'; - @override String get incompleteConnectionInfo => 'Server verbinding informatie is incompleet. Probeer opnieuw.'; - @override String malformedServerInfo({required Object message}) => 'Server informatie is verkeerd geformatteerd: ${message}'; - @override String get networkConnectionFailed => 'Netwerkverbinding mislukt. Controleer je internetverbinding en probeer opnieuw.'; - @override String get authenticationFailed => 'Authenticatie mislukt. Log opnieuw in.'; - @override String get plexServiceUnavailable => 'Plex service niet beschikbaar. Probeer later opnieuw.'; - @override String failedToLoadServers({required Object error}) => 'Kon servers niet laden: ${error}'; + // Translations + @override + String get connectingToServer => 'Verbinden met server...'; + @override + String get connectingToAllServers => 'Verbinden met alle servers...'; + @override + String get connectToAllServers => 'Verbind met alle servers'; + @override + String get allServerConnectionsFailed => + 'Kon niet verbinden met servers. Controleer je netwerk en probeer opnieuw.'; + @override + String foundServers({required Object count}) => '${count} server(s) gevonden'; + @override + String get orSelectIndividually => 'Of selecteer een server individueel:'; + @override + String get serverDebugCopied => + 'Server debug gegevens gekopieerd naar klembord'; + @override + String get copyDebugData => 'Kopieer debug gegevens'; + @override + String get noServersFound => 'Geen servers gevonden'; + @override + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => 'Geen servers gevonden voor ${username} (${email})'; + @override + String malformedServerData({required Object count}) => + '${count} server(s) gevonden met verkeerde data. Geen geldige servers beschikbaar.'; + @override + String get incompleteServerInfo => + 'Sommige servers hebben incomplete informatie en zijn overgeslagen. Controleer je Plex.tv account.'; + @override + String get incompleteConnectionInfo => + 'Server verbinding informatie is incompleet. Probeer opnieuw.'; + @override + String malformedServerInfo({required Object message}) => + 'Server informatie is verkeerd geformatteerd: ${message}'; + @override + String get networkConnectionFailed => + 'Netwerkverbinding mislukt. Controleer je internetverbinding en probeer opnieuw.'; + @override + String get authenticationFailed => 'Authenticatie mislukt. Log opnieuw in.'; + @override + String get plexServiceUnavailable => + 'Plex service niet beschikbaar. Probeer later opnieuw.'; + @override + String failedToLoadServers({required Object error}) => + 'Kon servers niet laden: ${error}'; } // Path: hubDetail class _StringsHubDetailNl implements _StringsHubDetailEn { - _StringsHubDetailNl._(this._root); + _StringsHubDetailNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Titel'; - @override String get releaseYear => 'Uitgavejaar'; - @override String get dateAdded => 'Datum toegevoegd'; - @override String get rating => 'Beoordeling'; - @override String get noItemsFound => 'Geen items gevonden'; + // Translations + @override + String get title => 'Titel'; + @override + String get releaseYear => 'Uitgavejaar'; + @override + String get dateAdded => 'Datum toegevoegd'; + @override + String get rating => 'Beoordeling'; + @override + String get noItemsFound => 'Geen items gevonden'; } // Path: logs class _StringsLogsNl implements _StringsLogsEn { - _StringsLogsNl._(this._root); + _StringsLogsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Logbestanden'; - @override String get clearLogs => 'Wis logs'; - @override String get copyLogs => 'Kopieer logs'; - @override String get exportLogs => 'Exporteer logs'; - @override String get noLogsToShow => 'Geen logs om te tonen'; - @override String get error => 'Fout:'; - @override String get stackTrace => 'Stacktracering:'; + // Translations + @override + String get title => 'Logbestanden'; + @override + String get clearLogs => 'Wis logs'; + @override + String get copyLogs => 'Kopieer logs'; + @override + String get exportLogs => 'Exporteer logs'; + @override + String get noLogsToShow => 'Geen logs om te tonen'; + @override + String get error => 'Fout:'; + @override + String get stackTrace => 'Stacktracering:'; } // Path: licenses class _StringsLicensesNl implements _StringsLicensesEn { - _StringsLicensesNl._(this._root); + _StringsLicensesNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get relatedPackages => 'Gerelateerde pakketten'; - @override String get license => 'Licentie'; - @override String licenseNumber({required Object number}) => 'Licentie ${number}'; - @override String licensesCount({required Object count}) => '${count} licenties'; + // Translations + @override + String get relatedPackages => 'Gerelateerde pakketten'; + @override + String get license => 'Licentie'; + @override + String licenseNumber({required Object number}) => 'Licentie ${number}'; + @override + String licensesCount({required Object count}) => '${count} licenties'; } // Path: navigation class _StringsNavigationNl implements _StringsNavigationEn { - _StringsNavigationNl._(this._root); + _StringsNavigationNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get home => 'Thuis'; - @override String get search => 'Zoeken'; - @override String get libraries => 'Bibliotheken'; - @override String get settings => 'Instellingen'; + // Translations + @override + String get home => 'Thuis'; + @override + String get search => 'Zoeken'; + @override + String get libraries => 'Bibliotheken'; + @override + String get settings => 'Instellingen'; + @override + String get downloads => 'Downloads'; +} + +// Path: downloads +class _StringsDownloadsNl implements _StringsDownloadsEn { + _StringsDownloadsNl._(this._root); + + @override + final _StringsNl _root; // ignore: unused_field + + // Translations + @override + String get title => 'Downloads'; + @override + String get manage => 'Beheren'; + @override + String get tvShows => 'Series'; + @override + String get movies => 'Films'; + @override + String get queue => 'Wachtrij'; + @override + String get noDownloads => 'Nog geen downloads'; + @override + String get noDownloadsDescription => + 'Gedownloade content verschijnt hier voor offline weergave'; + @override + String get browseContent => 'Blader door content'; + @override + String get downloading => 'Downloaden'; + @override + String get downloaded => 'Gedownload'; + @override + String get paused => 'Gepauzeerd'; + @override + String get failed => 'Mislukt'; + @override + String get queued => 'In wachtrij'; + @override + String get downloadNow => 'Download'; + @override + String get deleteDownload => 'Download verwijderen'; + @override + String get cancelDownload => 'Download annuleren'; + @override + String get pauseDownload => 'Download pauzeren'; + @override + String get resumeDownload => 'Download hervatten'; + @override + String get retryDownload => 'Download opnieuw proberen'; + @override + String get downloadComplete => 'Download voltooid'; + @override + String get downloadFailed => 'Download mislukt'; + @override + String get downloadCancelled => 'Download geannuleerd'; + @override + String get downloadQueued => 'Download in wachtrij'; + @override + String episodesQueued({required Object count}) => + '${count} afleveringen in wachtrij voor download'; + @override + String get downloadDeleted => 'Download verwijderd'; + @override + String get deleteConfirmTitle => 'Download verwijderen?'; + @override + String deleteConfirm({required Object title}) => + 'Weet je zeker dat je "${title}" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.'; + @override + String get deleteConfirmMessage => + 'Het gedownloade bestand wordt van je apparaat verwijderd.'; + @override + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => 'Verwijderen van ${title}... (${current} van ${total})'; + @override + String deletingSimple({required Object title}) => + 'Verwijderen van ${title}...'; + @override + String get storageUsed => 'Opslag gebruikt'; + @override + String get availableOffline => 'Offline beschikbaar'; + @override + String get downloadingSeason => 'Seizoen downloaden'; + @override + String get downloadingEpisode => 'Aflevering downloaden'; +} + +// Path: offline +class _StringsOfflineNl implements _StringsOfflineEn { + _StringsOfflineNl._(this._root); + + @override + final _StringsNl _root; // ignore: unused_field + + // Translations + @override + String get youAreOffline => 'Je bent offline'; + @override + String get offlineMode => 'Offline modus'; + @override + String get backOnline => 'Weer online'; + @override + String get showingDownloadedContent => + 'Alleen gedownloade content wordt getoond'; + @override + String get noInternetConnection => 'Geen internetverbinding'; + @override + String get serversUnreachable => 'Plex servers onbereikbaar'; + @override + String get connectToDownload => + 'Maak verbinding met internet om content te downloaden'; } // Path: playlists class _StringsPlaylistsNl implements _StringsPlaylistsEn { - _StringsPlaylistsNl._(this._root); + _StringsPlaylistsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Afspeellijsten'; - @override String get noPlaylists => 'Geen afspeellijsten gevonden'; - @override String get create => 'Afspeellijst maken'; - @override String get newPlaylist => 'Nieuwe afspeellijst'; - @override String get playlistName => 'Naam afspeellijst'; - @override String get enterPlaylistName => 'Voer naam afspeellijst in'; - @override String get edit => 'Afspeellijst bewerken'; - @override String get delete => 'Afspeellijst verwijderen'; - @override String get addTo => 'Toevoegen aan afspeellijst'; - @override String get addItems => 'Items toevoegen'; - @override String get removeItem => 'Verwijderen uit afspeellijst'; - @override String get clearPlaylist => 'Afspeellijst wissen'; - @override String get playAll => 'Alles afspelen'; - @override String get shuffle => 'Willekeurig'; - @override String get smartPlaylist => 'Slimme afspeellijst'; - @override String get regularPlaylist => 'Normale afspeellijst'; - @override String itemCount({required Object count}) => '${count} items'; - @override String get oneItem => '1 item'; - @override String get emptyPlaylist => 'Deze afspeellijst is leeg'; - @override String get deleteConfirm => 'Afspeellijst verwijderen?'; - @override String deleteMessage({required Object name}) => 'Weet je zeker dat je "${name}" wilt verwijderen?'; - @override String get created => 'Afspeellijst gemaakt'; - @override String get updated => 'Afspeellijst bijgewerkt'; - @override String get deleted => 'Afspeellijst verwijderd'; - @override String get itemAdded => 'Toegevoegd aan afspeellijst'; - @override String get itemRemoved => 'Verwijderd uit afspeellijst'; - @override String get selectPlaylist => 'Selecteer afspeellijst'; - @override String get createNewPlaylist => 'Nieuwe afspeellijst maken'; - @override String get errorCreating => 'Fout bij maken afspeellijst'; - @override String get errorDeleting => 'Fout bij verwijderen afspeellijst'; - @override String get errorLoading => 'Fout bij laden afspeellijsten'; - @override String get errorAdding => 'Fout bij toevoegen aan afspeellijst'; - @override String get errorReordering => 'Fout bij herschikken van afspeellijstitem'; - @override String get errorRemoving => 'Fout bij verwijderen uit afspeellijst'; - @override String get playlist => 'Afspeellijst'; + // Translations + @override + String get title => 'Afspeellijsten'; + @override + String get noPlaylists => 'Geen afspeellijsten gevonden'; + @override + String get create => 'Afspeellijst maken'; + @override + String get newPlaylist => 'Nieuwe afspeellijst'; + @override + String get playlistName => 'Naam afspeellijst'; + @override + String get enterPlaylistName => 'Voer naam afspeellijst in'; + @override + String get edit => 'Afspeellijst bewerken'; + @override + String get delete => 'Afspeellijst verwijderen'; + @override + String get addTo => 'Toevoegen aan afspeellijst'; + @override + String get addItems => 'Items toevoegen'; + @override + String get removeItem => 'Verwijderen uit afspeellijst'; + @override + String get clearPlaylist => 'Afspeellijst wissen'; + @override + String get playAll => 'Alles afspelen'; + @override + String get shuffle => 'Willekeurig'; + @override + String get smartPlaylist => 'Slimme afspeellijst'; + @override + String get regularPlaylist => 'Normale afspeellijst'; + @override + String itemCount({required Object count}) => '${count} items'; + @override + String get oneItem => '1 item'; + @override + String get emptyPlaylist => 'Deze afspeellijst is leeg'; + @override + String get deleteConfirm => 'Afspeellijst verwijderen?'; + @override + String deleteMessage({required Object name}) => + 'Weet je zeker dat je "${name}" wilt verwijderen?'; + @override + String get created => 'Afspeellijst gemaakt'; + @override + String get updated => 'Afspeellijst bijgewerkt'; + @override + String get deleted => 'Afspeellijst verwijderd'; + @override + String get itemAdded => 'Toegevoegd aan afspeellijst'; + @override + String get itemRemoved => 'Verwijderd uit afspeellijst'; + @override + String get selectPlaylist => 'Selecteer afspeellijst'; + @override + String get createNewPlaylist => 'Nieuwe afspeellijst maken'; + @override + String get errorCreating => 'Fout bij maken afspeellijst'; + @override + String get errorDeleting => 'Fout bij verwijderen afspeellijst'; + @override + String get errorLoading => 'Fout bij laden afspeellijsten'; + @override + String get errorAdding => 'Fout bij toevoegen aan afspeellijst'; + @override + String get errorReordering => 'Fout bij herschikken van afspeellijstitem'; + @override + String get errorRemoving => 'Fout bij verwijderen uit afspeellijst'; + @override + String get playlist => 'Afspeellijst'; } // Path: collections class _StringsCollectionsNl implements _StringsCollectionsEn { - _StringsCollectionsNl._(this._root); + _StringsCollectionsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get title => 'Collecties'; - @override String get collection => 'Collectie'; - @override String get empty => 'Collectie is leeg'; - @override String get noItems => 'Geen items in deze collectie'; - @override String get unknownLibrarySection => 'Kan niet verwijderen: onbekende bibliotheeksectie'; - @override String get deleteCollection => 'Collectie verwijderen'; - @override String deleteConfirm({required Object title}) => 'Weet je zeker dat je "${title}" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.'; - @override String get deleted => 'Collectie verwijderd'; - @override String get deleteFailed => 'Collectie verwijderen mislukt'; - @override String deleteFailedWithError({required Object error}) => 'Collectie verwijderen mislukt: ${error}'; - @override String failedToLoadItems({required Object error}) => 'Collectie-items laden mislukt: ${error}'; - @override String get addTo => 'Toevoegen aan collectie'; - @override String get selectCollection => 'Selecteer collectie'; - @override String get createNewCollection => 'Nieuwe collectie maken'; - @override String get collectionName => 'Collectienaam'; - @override String get enterCollectionName => 'Voer collectienaam in'; - @override String get addedToCollection => 'Toegevoegd aan collectie'; - @override String get errorAddingToCollection => 'Fout bij toevoegen aan collectie'; - @override String get created => 'Collectie gemaakt'; - @override String get removeFromCollection => 'Verwijderen uit collectie'; - @override String removeFromCollectionConfirm({required Object title}) => '"${title}" uit deze collectie verwijderen?'; - @override String get removedFromCollection => 'Uit collectie verwijderd'; - @override String get removeFromCollectionFailed => 'Verwijderen uit collectie mislukt'; - @override String removeFromCollectionError({required Object error}) => 'Fout bij verwijderen uit collectie: ${error}'; + // Translations + @override + String get title => 'Collecties'; + @override + String get collection => 'Collectie'; + @override + String get empty => 'Collectie is leeg'; + @override + String get noItems => 'Geen items in deze collectie'; + @override + String get unknownLibrarySection => + 'Kan niet verwijderen: onbekende bibliotheeksectie'; + @override + String get deleteCollection => 'Collectie verwijderen'; + @override + String deleteConfirm({required Object title}) => + 'Weet je zeker dat je "${title}" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.'; + @override + String get deleted => 'Collectie verwijderd'; + @override + String get deleteFailed => 'Collectie verwijderen mislukt'; + @override + String deleteFailedWithError({required Object error}) => + 'Collectie verwijderen mislukt: ${error}'; + @override + String failedToLoadItems({required Object error}) => + 'Collectie-items laden mislukt: ${error}'; + @override + String get addTo => 'Toevoegen aan collectie'; + @override + String get selectCollection => 'Selecteer collectie'; + @override + String get createNewCollection => 'Nieuwe collectie maken'; + @override + String get collectionName => 'Collectienaam'; + @override + String get enterCollectionName => 'Voer collectienaam in'; + @override + String get addedToCollection => 'Toegevoegd aan collectie'; + @override + String get errorAddingToCollection => 'Fout bij toevoegen aan collectie'; + @override + String get created => 'Collectie gemaakt'; + @override + String get removeFromCollection => 'Verwijderen uit collectie'; + @override + String removeFromCollectionConfirm({required Object title}) => + '"${title}" uit deze collectie verwijderen?'; + @override + String get removedFromCollection => 'Uit collectie verwijderd'; + @override + String get removeFromCollectionFailed => 'Verwijderen uit collectie mislukt'; + @override + String removeFromCollectionError({required Object error}) => + 'Fout bij verwijderen uit collectie: ${error}'; } // Path: libraries.tabs class _StringsLibrariesTabsNl implements _StringsLibrariesTabsEn { - _StringsLibrariesTabsNl._(this._root); + _StringsLibrariesTabsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get recommended => 'Aanbevolen'; - @override String get browse => 'Bladeren'; - @override String get collections => 'Collecties'; - @override String get playlists => 'Afspeellijsten'; + // Translations + @override + String get recommended => 'Aanbevolen'; + @override + String get browse => 'Bladeren'; + @override + String get collections => 'Collecties'; + @override + String get playlists => 'Afspeellijsten'; } // Path: libraries.groupings class _StringsLibrariesGroupingsNl implements _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsNl._(this._root); + _StringsLibrariesGroupingsNl._(this._root); - @override final _StringsNl _root; // ignore: unused_field + @override + final _StringsNl _root; // ignore: unused_field - // Translations - @override String get all => 'Alles'; - @override String get movies => 'Films'; - @override String get shows => 'Series'; - @override String get seasons => 'Seizoenen'; - @override String get episodes => 'Afleveringen'; - @override String get folders => 'Mappen'; + // Translations + @override + String get all => 'Alles'; + @override + String get movies => 'Films'; + @override + String get shows => 'Series'; + @override + String get seasons => 'Seizoenen'; + @override + String get episodes => 'Afleveringen'; + @override + String get folders => 'Mappen'; } // Path: class _StringsSv implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsSv.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.sv, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsSv.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.sv, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); - @override late final _StringsSv _root = this; // ignore: unused_field + @override + late final _StringsSv _root = this; // ignore: unused_field - // Translations - @override late final _StringsAppSv app = _StringsAppSv._(_root); - @override late final _StringsAuthSv auth = _StringsAuthSv._(_root); - @override late final _StringsCommonSv common = _StringsCommonSv._(_root); - @override late final _StringsScreensSv screens = _StringsScreensSv._(_root); - @override late final _StringsUpdateSv update = _StringsUpdateSv._(_root); - @override late final _StringsSettingsSv settings = _StringsSettingsSv._(_root); - @override late final _StringsSearchSv search = _StringsSearchSv._(_root); - @override late final _StringsHotkeysSv hotkeys = _StringsHotkeysSv._(_root); - @override late final _StringsPinEntrySv pinEntry = _StringsPinEntrySv._(_root); - @override late final _StringsFileInfoSv fileInfo = _StringsFileInfoSv._(_root); - @override late final _StringsMediaMenuSv mediaMenu = _StringsMediaMenuSv._(_root); - @override late final _StringsAccessibilitySv accessibility = _StringsAccessibilitySv._(_root); - @override late final _StringsTooltipsSv tooltips = _StringsTooltipsSv._(_root); - @override late final _StringsVideoControlsSv videoControls = _StringsVideoControlsSv._(_root); - @override late final _StringsUserStatusSv userStatus = _StringsUserStatusSv._(_root); - @override late final _StringsMessagesSv messages = _StringsMessagesSv._(_root); - @override late final _StringsProfileSv profile = _StringsProfileSv._(_root); - @override late final _StringsSubtitlingStylingSv subtitlingStyling = _StringsSubtitlingStylingSv._(_root); - @override late final _StringsDialogSv dialog = _StringsDialogSv._(_root); - @override late final _StringsDiscoverSv discover = _StringsDiscoverSv._(_root); - @override late final _StringsErrorsSv errors = _StringsErrorsSv._(_root); - @override late final _StringsLibrariesSv libraries = _StringsLibrariesSv._(_root); - @override late final _StringsAboutSv about = _StringsAboutSv._(_root); - @override late final _StringsServerSelectionSv serverSelection = _StringsServerSelectionSv._(_root); - @override late final _StringsHubDetailSv hubDetail = _StringsHubDetailSv._(_root); - @override late final _StringsLogsSv logs = _StringsLogsSv._(_root); - @override late final _StringsLicensesSv licenses = _StringsLicensesSv._(_root); - @override late final _StringsNavigationSv navigation = _StringsNavigationSv._(_root); - @override late final _StringsPlaylistsSv playlists = _StringsPlaylistsSv._(_root); - @override late final _StringsCollectionsSv collections = _StringsCollectionsSv._(_root); + // Translations + @override + late final _StringsAppSv app = _StringsAppSv._(_root); + @override + late final _StringsAuthSv auth = _StringsAuthSv._(_root); + @override + late final _StringsCommonSv common = _StringsCommonSv._(_root); + @override + late final _StringsScreensSv screens = _StringsScreensSv._(_root); + @override + late final _StringsUpdateSv update = _StringsUpdateSv._(_root); + @override + late final _StringsSettingsSv settings = _StringsSettingsSv._(_root); + @override + late final _StringsSearchSv search = _StringsSearchSv._(_root); + @override + late final _StringsHotkeysSv hotkeys = _StringsHotkeysSv._(_root); + @override + late final _StringsPinEntrySv pinEntry = _StringsPinEntrySv._(_root); + @override + late final _StringsFileInfoSv fileInfo = _StringsFileInfoSv._(_root); + @override + late final _StringsMediaMenuSv mediaMenu = _StringsMediaMenuSv._(_root); + @override + late final _StringsAccessibilitySv accessibility = _StringsAccessibilitySv._( + _root, + ); + @override + late final _StringsTooltipsSv tooltips = _StringsTooltipsSv._(_root); + @override + late final _StringsVideoControlsSv videoControls = _StringsVideoControlsSv._( + _root, + ); + @override + late final _StringsUserStatusSv userStatus = _StringsUserStatusSv._(_root); + @override + late final _StringsMessagesSv messages = _StringsMessagesSv._(_root); + @override + late final _StringsProfileSv profile = _StringsProfileSv._(_root); + @override + late final _StringsSubtitlingStylingSv subtitlingStyling = + _StringsSubtitlingStylingSv._(_root); + @override + late final _StringsDialogSv dialog = _StringsDialogSv._(_root); + @override + late final _StringsDiscoverSv discover = _StringsDiscoverSv._(_root); + @override + late final _StringsErrorsSv errors = _StringsErrorsSv._(_root); + @override + late final _StringsLibrariesSv libraries = _StringsLibrariesSv._(_root); + @override + late final _StringsAboutSv about = _StringsAboutSv._(_root); + @override + late final _StringsServerSelectionSv serverSelection = + _StringsServerSelectionSv._(_root); + @override + late final _StringsHubDetailSv hubDetail = _StringsHubDetailSv._(_root); + @override + late final _StringsLogsSv logs = _StringsLogsSv._(_root); + @override + late final _StringsLicensesSv licenses = _StringsLicensesSv._(_root); + @override + late final _StringsNavigationSv navigation = _StringsNavigationSv._(_root); + @override + late final _StringsDownloadsSv downloads = _StringsDownloadsSv._(_root); + @override + late final _StringsOfflineSv offline = _StringsOfflineSv._(_root); + @override + late final _StringsPlaylistsSv playlists = _StringsPlaylistsSv._(_root); + @override + late final _StringsCollectionsSv collections = _StringsCollectionsSv._(_root); } // Path: app class _StringsAppSv implements _StringsAppEn { - _StringsAppSv._(this._root); + _StringsAppSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Plezy'; - @override String get loading => 'Laddar...'; + // Translations + @override + String get title => 'Plezy'; + @override + String get loading => 'Laddar...'; } // Path: auth class _StringsAuthSv implements _StringsAuthEn { - _StringsAuthSv._(this._root); + _StringsAuthSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get signInWithPlex => 'Logga in med Plex'; - @override String get showQRCode => 'Visa QR-kod'; - @override String get cancel => 'Avbryt'; - @override String get authenticate => 'Autentisera'; - @override String get retry => 'Försök igen'; - @override String get debugEnterToken => 'Debug: Ange Plex-token'; - @override String get plexTokenLabel => 'Plex-autentiseringstoken'; - @override String get plexTokenHint => 'Ange din Plex.tv-token'; - @override String get authenticationTimeout => 'Autentisering tog för lång tid. Försök igen.'; - @override String get scanQRCodeInstruction => 'Skanna denna QR-kod med en enhet inloggad på Plex för att autentisera.'; - @override String get waitingForAuth => 'Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.'; + // Translations + @override + String get signInWithPlex => 'Logga in med Plex'; + @override + String get showQRCode => 'Visa QR-kod'; + @override + String get cancel => 'Avbryt'; + @override + String get authenticate => 'Autentisera'; + @override + String get retry => 'Försök igen'; + @override + String get debugEnterToken => 'Debug: Ange Plex-token'; + @override + String get plexTokenLabel => 'Plex-autentiseringstoken'; + @override + String get plexTokenHint => 'Ange din Plex.tv-token'; + @override + String get authenticationTimeout => + 'Autentisering tog för lång tid. Försök igen.'; + @override + String get scanQRCodeInstruction => + 'Skanna denna QR-kod med en enhet inloggad på Plex för att autentisera.'; + @override + String get waitingForAuth => + 'Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.'; } // Path: common class _StringsCommonSv implements _StringsCommonEn { - _StringsCommonSv._(this._root); + _StringsCommonSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get cancel => 'Avbryt'; - @override String get save => 'Spara'; - @override String get close => 'Stäng'; - @override String get clear => 'Rensa'; - @override String get reset => 'Återställ'; - @override String get later => 'Senare'; - @override String get submit => 'Skicka'; - @override String get confirm => 'Bekräfta'; - @override String get retry => 'Försök igen'; - @override String get playNow => 'Spela nu'; - @override String get logout => 'Logga ut'; - @override String get online => 'Online'; - @override String get offline => 'Offline'; - @override String get owned => 'Egen'; - @override String get shared => 'Delad'; - @override String get current => 'NUVARANDE'; - @override String get unknown => 'Okänd'; - @override String get refresh => 'Uppdatera'; - @override String get yes => 'Ja'; - @override String get no => 'Nej'; - @override String get server => 'Server'; - @override String get delete => 'Ta bort'; - @override String get shuffle => 'Blanda'; - @override String get addTo => 'Lägg till i...'; + // Translations + @override + String get cancel => 'Avbryt'; + @override + String get save => 'Spara'; + @override + String get close => 'Stäng'; + @override + String get clear => 'Rensa'; + @override + String get reset => 'Återställ'; + @override + String get later => 'Senare'; + @override + String get submit => 'Skicka'; + @override + String get confirm => 'Bekräfta'; + @override + String get retry => 'Försök igen'; + @override + String get playNow => 'Spela nu'; + @override + String get logout => 'Logga ut'; + @override + String get online => 'Online'; + @override + String get offline => 'Offline'; + @override + String get owned => 'Egen'; + @override + String get shared => 'Delad'; + @override + String get current => 'NUVARANDE'; + @override + String get unknown => 'Okänd'; + @override + String get refresh => 'Uppdatera'; + @override + String get yes => 'Ja'; + @override + String get no => 'Nej'; + @override + String get server => 'Server'; + @override + String get delete => 'Ta bort'; + @override + String get shuffle => 'Blanda'; + @override + String get addTo => 'Lägg till i...'; } // Path: screens class _StringsScreensSv implements _StringsScreensEn { - _StringsScreensSv._(this._root); + _StringsScreensSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get licenses => 'Licenser'; - @override String get selectServer => 'Välj server'; - @override String get switchProfile => 'Byt profil'; - @override String get subtitleStyling => 'Undertext-styling'; - @override String get search => 'Sök'; - @override String get logs => 'Loggar'; + // Translations + @override + String get licenses => 'Licenser'; + @override + String get selectServer => 'Välj server'; + @override + String get switchProfile => 'Byt profil'; + @override + String get subtitleStyling => 'Undertext-styling'; + @override + String get search => 'Sök'; + @override + String get logs => 'Loggar'; } // Path: update class _StringsUpdateSv implements _StringsUpdateEn { - _StringsUpdateSv._(this._root); + _StringsUpdateSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get available => 'Uppdatering tillgänglig'; - @override String versionAvailable({required Object version}) => 'Version ${version} är tillgänglig'; - @override String currentVersion({required Object version}) => 'Nuvarande: ${version}'; - @override String get skipVersion => 'Hoppa över denna version'; - @override String get viewRelease => 'Visa release'; - @override String get latestVersion => 'Du har den senaste versionen'; - @override String get checkFailed => 'Misslyckades att kontrollera uppdateringar'; + // Translations + @override + String get available => 'Uppdatering tillgänglig'; + @override + String versionAvailable({required Object version}) => + 'Version ${version} är tillgänglig'; + @override + String currentVersion({required Object version}) => 'Nuvarande: ${version}'; + @override + String get skipVersion => 'Hoppa över denna version'; + @override + String get viewRelease => 'Visa release'; + @override + String get latestVersion => 'Du har den senaste versionen'; + @override + String get checkFailed => 'Misslyckades att kontrollera uppdateringar'; } // Path: settings class _StringsSettingsSv implements _StringsSettingsEn { - _StringsSettingsSv._(this._root); + _StringsSettingsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Inställningar'; - @override String get language => 'Språk'; - @override String get theme => 'Tema'; - @override String get appearance => 'Utseende'; - @override String get videoPlayback => 'Videouppspelning'; - @override String get shufflePlay => 'Blanda uppspelning'; - @override String get advanced => 'Avancerat'; - @override String get useSeasonPostersDescription => 'Visa säsongsaffisch istället för serieaffisch för avsnitt'; - @override String get showHeroSectionDescription => 'Visa utvalda innehållskarusell på startsidan'; - @override String get secondsLabel => 'Sekunder'; - @override String get minutesLabel => 'Minuter'; - @override String get secondsShort => 's'; - @override String get minutesShort => 'm'; - @override String durationHint({required Object min, required Object max}) => 'Ange tid (${min}-${max})'; - @override String get systemTheme => 'System'; - @override String get systemThemeDescription => 'Följ systeminställningar'; - @override String get lightTheme => 'Ljust'; - @override String get darkTheme => 'Mörkt'; - @override String get libraryDensity => 'Biblioteksdensitet'; - @override String get compact => 'Kompakt'; - @override String get compactDescription => 'Mindre kort, fler objekt synliga'; - @override String get normal => 'Normal'; - @override String get normalDescription => 'Standardstorlek'; - @override String get comfortable => 'Bekväm'; - @override String get comfortableDescription => 'Större kort, färre objekt synliga'; - @override String get viewMode => 'Visningsläge'; - @override String get gridView => 'Rutnät'; - @override String get gridViewDescription => 'Visa objekt i rutnätslayout'; - @override String get listView => 'Lista'; - @override String get listViewDescription => 'Visa objekt i listlayout'; - @override String get useSeasonPosters => 'Använd säsongsaffischer'; - @override String get showHeroSection => 'Visa hjältesektion'; - @override String get hardwareDecoding => 'Hårdvaruavkodning'; - @override String get hardwareDecodingDescription => 'Använd hårdvaruacceleration när tillgängligt'; - @override String get bufferSize => 'Bufferstorlek'; - @override String bufferSizeMB({required Object size}) => '${size}MB'; - @override String get subtitleStyling => 'Undertext-styling'; - @override String get subtitleStylingDescription => 'Anpassa undertextutseende'; - @override String get smallSkipDuration => 'Kort hoppvaraktighet'; - @override String get largeSkipDuration => 'Lång hoppvaraktighet'; - @override String secondsUnit({required Object seconds}) => '${seconds} sekunder'; - @override String get defaultSleepTimer => 'Standard sovtimer'; - @override String minutesUnit({required Object minutes}) => '${minutes} minuter'; - @override String get rememberTrackSelections => 'Kom ihåg spårval per serie/film'; - @override String get rememberTrackSelectionsDescription => 'Spara automatiskt ljud- och undertextspråkpreferenser när du ändrar spår under uppspelning'; - @override String get unwatchedOnly => 'Endast osedda'; - @override String get unwatchedOnlyDescription => 'Inkludera endast osedda avsnitt i blandningskön'; - @override String get shuffleOrderNavigation => 'Blandningsordning-navigation'; - @override String get shuffleOrderNavigationDescription => 'Nästa/föregående knappar följer blandad ordning'; - @override String get loopShuffleQueue => 'Loopa blandningskö'; - @override String get loopShuffleQueueDescription => 'Starta om kö när slutet nås'; - @override String get videoPlayerControls => 'Videospelar-kontroller'; - @override String get keyboardShortcuts => 'Tangentbordsgenvägar'; - @override String get keyboardShortcutsDescription => 'Anpassa tangentbordsgenvägar'; - @override String get debugLogging => 'Felsökningsloggning'; - @override String get debugLoggingDescription => 'Aktivera detaljerad loggning för felsökning'; - @override String get viewLogs => 'Visa loggar'; - @override String get viewLogsDescription => 'Visa applikationsloggar'; - @override String get clearCache => 'Rensa cache'; - @override String get clearCacheDescription => 'Detta rensar alla cachade bilder och data. Appen kan ta längre tid att ladda innehåll efter cache-rensning.'; - @override String get clearCacheSuccess => 'Cache rensad framgångsrikt'; - @override String get resetSettings => 'Återställ inställningar'; - @override String get resetSettingsDescription => 'Detta återställer alla inställningar till standardvärden. Denna åtgärd kan inte ångras.'; - @override String get resetSettingsSuccess => 'Inställningar återställda framgångsrikt'; - @override String get shortcutsReset => 'Genvägar återställda till standard'; - @override String get about => 'Om'; - @override String get aboutDescription => 'Appinformation och licenser'; - @override String get updates => 'Uppdateringar'; - @override String get updateAvailable => 'Uppdatering tillgänglig'; - @override String get checkForUpdates => 'Kontrollera uppdateringar'; - @override String get validationErrorEnterNumber => 'Vänligen ange ett giltigt nummer'; - @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 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'; - @override String get autoSkipCredits => 'Hoppa Över Credits Automatiskt'; - @override String get autoSkipCreditsDescription => 'Hoppa automatiskt över credits och spela nästa avsnitt'; - @override String get autoSkipDelay => 'Fördröjning Auto Hoppa Över'; - @override String autoSkipDelayDescription({required Object seconds}) => 'Vänta ${seconds} sekunder innan automatisk överhoppning'; + // Translations + @override + String get title => 'Inställningar'; + @override + String get language => 'Språk'; + @override + String get theme => 'Tema'; + @override + String get appearance => 'Utseende'; + @override + String get videoPlayback => 'Videouppspelning'; + @override + String get shufflePlay => 'Blanda uppspelning'; + @override + String get advanced => 'Avancerat'; + @override + String get useSeasonPostersDescription => + 'Visa säsongsaffisch istället för serieaffisch för avsnitt'; + @override + String get showHeroSectionDescription => + 'Visa utvalda innehållskarusell på startsidan'; + @override + String get secondsLabel => 'Sekunder'; + @override + String get minutesLabel => 'Minuter'; + @override + String get secondsShort => 's'; + @override + String get minutesShort => 'm'; + @override + String durationHint({required Object min, required Object max}) => + 'Ange tid (${min}-${max})'; + @override + String get systemTheme => 'System'; + @override + String get systemThemeDescription => 'Följ systeminställningar'; + @override + String get lightTheme => 'Ljust'; + @override + String get darkTheme => 'Mörkt'; + @override + String get libraryDensity => 'Biblioteksdensitet'; + @override + String get compact => 'Kompakt'; + @override + String get compactDescription => 'Mindre kort, fler objekt synliga'; + @override + String get normal => 'Normal'; + @override + String get normalDescription => 'Standardstorlek'; + @override + String get comfortable => 'Bekväm'; + @override + String get comfortableDescription => 'Större kort, färre objekt synliga'; + @override + String get viewMode => 'Visningsläge'; + @override + String get gridView => 'Rutnät'; + @override + String get gridViewDescription => 'Visa objekt i rutnätslayout'; + @override + String get listView => 'Lista'; + @override + String get listViewDescription => 'Visa objekt i listlayout'; + @override + String get useSeasonPosters => 'Använd säsongsaffischer'; + @override + String get showHeroSection => 'Visa hjältesektion'; + @override + String get hardwareDecoding => 'Hårdvaruavkodning'; + @override + String get hardwareDecodingDescription => + 'Använd hårdvaruacceleration när tillgängligt'; + @override + String get bufferSize => 'Bufferstorlek'; + @override + String bufferSizeMB({required Object size}) => '${size}MB'; + @override + String get subtitleStyling => 'Undertext-styling'; + @override + String get subtitleStylingDescription => 'Anpassa undertextutseende'; + @override + String get smallSkipDuration => 'Kort hoppvaraktighet'; + @override + String get largeSkipDuration => 'Lång hoppvaraktighet'; + @override + String secondsUnit({required Object seconds}) => '${seconds} sekunder'; + @override + String get defaultSleepTimer => 'Standard sovtimer'; + @override + String minutesUnit({required Object minutes}) => '${minutes} minuter'; + @override + String get rememberTrackSelections => 'Kom ihåg spårval per serie/film'; + @override + String get rememberTrackSelectionsDescription => + 'Spara automatiskt ljud- och undertextspråkpreferenser när du ändrar spår under uppspelning'; + @override + String get unwatchedOnly => 'Endast osedda'; + @override + String get unwatchedOnlyDescription => + 'Inkludera endast osedda avsnitt i blandningskön'; + @override + String get shuffleOrderNavigation => 'Blandningsordning-navigation'; + @override + String get shuffleOrderNavigationDescription => + 'Nästa/föregående knappar följer blandad ordning'; + @override + String get loopShuffleQueue => 'Loopa blandningskö'; + @override + String get loopShuffleQueueDescription => 'Starta om kö när slutet nås'; + @override + String get videoPlayerControls => 'Videospelar-kontroller'; + @override + String get keyboardShortcuts => 'Tangentbordsgenvägar'; + @override + String get keyboardShortcutsDescription => 'Anpassa tangentbordsgenvägar'; + @override + String get debugLogging => 'Felsökningsloggning'; + @override + String get debugLoggingDescription => + 'Aktivera detaljerad loggning för felsökning'; + @override + String get viewLogs => 'Visa loggar'; + @override + String get viewLogsDescription => 'Visa applikationsloggar'; + @override + String get clearCache => 'Rensa cache'; + @override + String get clearCacheDescription => + 'Detta rensar alla cachade bilder och data. Appen kan ta längre tid att ladda innehåll efter cache-rensning.'; + @override + String get clearCacheSuccess => 'Cache rensad framgångsrikt'; + @override + String get resetSettings => 'Återställ inställningar'; + @override + String get resetSettingsDescription => + 'Detta återställer alla inställningar till standardvärden. Denna åtgärd kan inte ångras.'; + @override + String get resetSettingsSuccess => 'Inställningar återställda framgångsrikt'; + @override + String get shortcutsReset => 'Genvägar återställda till standard'; + @override + String get about => 'Om'; + @override + String get aboutDescription => 'Appinformation och licenser'; + @override + String get updates => 'Uppdateringar'; + @override + String get updateAvailable => 'Uppdatering tillgänglig'; + @override + String get checkForUpdates => 'Kontrollera uppdateringar'; + @override + String get validationErrorEnterNumber => 'Vänligen ange ett giltigt nummer'; + @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 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'; + @override + String get autoSkipCredits => 'Hoppa Över Credits Automatiskt'; + @override + String get autoSkipCreditsDescription => + 'Hoppa automatiskt över credits och spela nästa avsnitt'; + @override + String get autoSkipDelay => 'Fördröjning Auto Hoppa Över'; + @override + String autoSkipDelayDescription({required Object seconds}) => + 'Vänta ${seconds} sekunder innan automatisk överhoppning'; + @override + String get downloads => 'Nedladdningar'; + @override + String get downloadLocationDescription => + 'Välj var nedladdat innehåll ska lagras'; + @override + String get downloadLocationDefault => 'Standard (App-lagring)'; + @override + String get downloadLocationCustom => 'Anpassad Plats'; + @override + String get selectFolder => 'Välj Mapp'; + @override + String get resetToDefault => 'Återställ till Standard'; + @override + String currentPath({required Object path}) => 'Nuvarande: ${path}'; + @override + String get downloadLocationNotSupported => + 'Anpassad nedladdningsplats stöds inte på iOS'; + @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 downloadOnWifiOnly => 'Ladda ner endast på WiFi'; + @override + String get downloadOnWifiOnlyDescription => + 'Förhindra nedladdningar vid användning av mobildata'; + @override + String get cellularDownloadBlocked => + 'Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.'; } // Path: search class _StringsSearchSv implements _StringsSearchEn { - _StringsSearchSv._(this._root); + _StringsSearchSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get hint => 'Sök filmer, serier, musik...'; - @override String get tryDifferentTerm => 'Prova en annan sökterm'; - @override String get searchYourMedia => 'Sök i dina media'; - @override String get enterTitleActorOrKeyword => 'Ange en titel, skådespelare eller nyckelord'; + // Translations + @override + String get hint => 'Sök filmer, serier, musik...'; + @override + String get tryDifferentTerm => 'Prova en annan sökterm'; + @override + String get searchYourMedia => 'Sök i dina media'; + @override + String get enterTitleActorOrKeyword => + 'Ange en titel, skådespelare eller nyckelord'; } // Path: hotkeys class _StringsHotkeysSv implements _StringsHotkeysEn { - _StringsHotkeysSv._(this._root); + _StringsHotkeysSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String setShortcutFor({required Object actionName}) => 'Sätt genväg för ${actionName}'; - @override String get clearShortcut => 'Rensa genväg'; + // Translations + @override + String setShortcutFor({required Object actionName}) => + 'Sätt genväg för ${actionName}'; + @override + String get clearShortcut => 'Rensa genväg'; } // Path: pinEntry class _StringsPinEntrySv implements _StringsPinEntryEn { - _StringsPinEntrySv._(this._root); + _StringsPinEntrySv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get enterPin => 'Ange PIN'; - @override String get showPin => 'Visa PIN'; - @override String get hidePin => 'Dölj PIN'; + // Translations + @override + String get enterPin => 'Ange PIN'; + @override + String get showPin => 'Visa PIN'; + @override + String get hidePin => 'Dölj PIN'; } // Path: fileInfo class _StringsFileInfoSv implements _StringsFileInfoEn { - _StringsFileInfoSv._(this._root); + _StringsFileInfoSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Filinformation'; - @override String get video => 'Video'; - @override String get audio => 'Ljud'; - @override String get file => 'Fil'; - @override String get advanced => 'Avancerat'; - @override String get codec => 'Kodek'; - @override String get resolution => 'Upplösning'; - @override String get bitrate => 'Bithastighet'; - @override String get frameRate => 'Bildfrekvens'; - @override String get aspectRatio => 'Bildförhållande'; - @override String get profile => 'Profil'; - @override String get bitDepth => 'Bitdjup'; - @override String get colorSpace => 'Färgrymd'; - @override String get colorRange => 'Färgområde'; - @override String get colorPrimaries => 'Färggrunder'; - @override String get chromaSubsampling => 'Kroma-undersampling'; - @override String get channels => 'Kanaler'; - @override String get path => 'Sökväg'; - @override String get size => 'Storlek'; - @override String get container => 'Container'; - @override String get duration => 'Varaktighet'; - @override String get optimizedForStreaming => 'Optimerad för streaming'; - @override String get has64bitOffsets => '64-bit offset'; + // Translations + @override + String get title => 'Filinformation'; + @override + String get video => 'Video'; + @override + String get audio => 'Ljud'; + @override + String get file => 'Fil'; + @override + String get advanced => 'Avancerat'; + @override + String get codec => 'Kodek'; + @override + String get resolution => 'Upplösning'; + @override + String get bitrate => 'Bithastighet'; + @override + String get frameRate => 'Bildfrekvens'; + @override + String get aspectRatio => 'Bildförhållande'; + @override + String get profile => 'Profil'; + @override + String get bitDepth => 'Bitdjup'; + @override + String get colorSpace => 'Färgrymd'; + @override + String get colorRange => 'Färgområde'; + @override + String get colorPrimaries => 'Färggrunder'; + @override + String get chromaSubsampling => 'Kroma-undersampling'; + @override + String get channels => 'Kanaler'; + @override + String get path => 'Sökväg'; + @override + String get size => 'Storlek'; + @override + String get container => 'Container'; + @override + String get duration => 'Varaktighet'; + @override + String get optimizedForStreaming => 'Optimerad för streaming'; + @override + String get has64bitOffsets => '64-bit offset'; } // Path: mediaMenu class _StringsMediaMenuSv implements _StringsMediaMenuEn { - _StringsMediaMenuSv._(this._root); + _StringsMediaMenuSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get markAsWatched => 'Markera som sedd'; - @override String get markAsUnwatched => 'Markera som osedd'; - @override String get removeFromContinueWatching => 'Ta bort från Fortsätt titta'; - @override String get goToSeries => 'Gå till serie'; - @override String get goToSeason => 'Gå till säsong'; - @override String get shufflePlay => 'Blanda uppspelning'; - @override String get fileInfo => 'Filinformation'; + // Translations + @override + String get markAsWatched => 'Markera som sedd'; + @override + String get markAsUnwatched => 'Markera som osedd'; + @override + String get removeFromContinueWatching => 'Ta bort från Fortsätt titta'; + @override + String get goToSeries => 'Gå till serie'; + @override + String get goToSeason => 'Gå till säsong'; + @override + String get shufflePlay => 'Blanda uppspelning'; + @override + String get fileInfo => 'Filinformation'; } // Path: accessibility class _StringsAccessibilitySv implements _StringsAccessibilityEn { - _StringsAccessibilitySv._(this._root); + _StringsAccessibilitySv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String mediaCardMovie({required Object title}) => '${title}, film'; - @override String mediaCardShow({required Object title}) => '${title}, TV-serie'; - @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - @override String get mediaCardWatched => 'sedd'; - @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} procent sedd'; - @override String get mediaCardUnwatched => 'osedd'; - @override String get tapToPlay => 'Tryck för att spela'; + // Translations + @override + String mediaCardMovie({required Object title}) => '${title}, film'; + @override + String mediaCardShow({required Object title}) => '${title}, TV-serie'; + @override + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + @override + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + @override + String get mediaCardWatched => 'sedd'; + @override + String mediaCardPartiallyWatched({required Object percent}) => + '${percent} procent sedd'; + @override + String get mediaCardUnwatched => 'osedd'; + @override + String get tapToPlay => 'Tryck för att spela'; } // Path: tooltips class _StringsTooltipsSv implements _StringsTooltipsEn { - _StringsTooltipsSv._(this._root); + _StringsTooltipsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get shufflePlay => 'Blanda uppspelning'; - @override String get markAsWatched => 'Markera som sedd'; - @override String get markAsUnwatched => 'Markera som osedd'; + // Translations + @override + String get shufflePlay => 'Blanda uppspelning'; + @override + String get markAsWatched => 'Markera som sedd'; + @override + String get markAsUnwatched => 'Markera som osedd'; } // Path: videoControls class _StringsVideoControlsSv implements _StringsVideoControlsEn { - _StringsVideoControlsSv._(this._root); + _StringsVideoControlsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get audioLabel => 'Ljud'; - @override String get subtitlesLabel => 'Undertexter'; - @override String get resetToZero => 'Återställ till 0ms'; - @override String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - @override String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - @override String playsLater({required Object label}) => '${label} spelas senare'; - @override String playsEarlier({required Object label}) => '${label} spelas tidigare'; - @override String get noOffset => 'Ingen offset'; - @override String get letterbox => 'Letterbox'; - @override String get fillScreen => 'Fyll skärm'; - @override String get stretch => 'Sträck'; - @override String get lockRotation => 'Lås rotation'; - @override String get unlockRotation => 'Lås upp rotation'; - @override String get sleepTimer => 'Sovtimer'; - @override String get timerActive => 'Timer aktiv'; - @override String playbackWillPauseIn({required Object duration}) => 'Uppspelningen pausas om ${duration}'; - @override String get sleepTimerCompleted => 'Sovtimer slutförd - uppspelning pausad'; - @override String get playButton => 'Spela'; - @override String get pauseButton => 'Pausa'; - @override String seekBackwardButton({required Object seconds}) => 'Spola bakåt ${seconds} sekunder'; - @override String seekForwardButton({required Object seconds}) => 'Spola framåt ${seconds} sekunder'; - @override String get previousButton => 'Föregående avsnitt'; - @override String get nextButton => 'Nästa avsnitt'; - @override String get previousChapterButton => 'Föregående kapitel'; - @override String get nextChapterButton => 'Nästa kapitel'; - @override String get volumeButton => 'Volym'; - @override String get muteButton => 'Tysta'; - @override String get unmuteButton => 'Slå på ljud'; - @override String get settingsButton => 'Videoinställningar'; - @override String get audioTrackButton => 'Ljudspår'; - @override String get subtitlesButton => 'Undertexter'; - @override String get chaptersButton => 'Kapitel'; - @override String get versionsButton => 'Videoversioner'; - @override String get aspectRatioButton => 'Bildförhållande'; - @override String get fullscreenButton => 'Aktivera helskärm'; - @override String get exitFullscreenButton => 'Avsluta helskärm'; - @override String get rotationLockButton => 'Rotationslås'; - @override String get timelineSlider => 'Videotidslinje'; - @override String get volumeSlider => 'Volymnivå'; - @override String get backButton => 'Tillbaka'; + // Translations + @override + String get audioLabel => 'Ljud'; + @override + String get subtitlesLabel => 'Undertexter'; + @override + String get resetToZero => 'Återställ till 0ms'; + @override + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + @override + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + @override + String playsLater({required Object label}) => '${label} spelas senare'; + @override + String playsEarlier({required Object label}) => '${label} spelas tidigare'; + @override + String get noOffset => 'Ingen offset'; + @override + String get letterbox => 'Letterbox'; + @override + String get fillScreen => 'Fyll skärm'; + @override + String get stretch => 'Sträck'; + @override + String get lockRotation => 'Lås rotation'; + @override + String get unlockRotation => 'Lås upp rotation'; + @override + String get sleepTimer => 'Sovtimer'; + @override + String get timerActive => 'Timer aktiv'; + @override + String playbackWillPauseIn({required Object duration}) => + 'Uppspelningen pausas om ${duration}'; + @override + String get sleepTimerCompleted => 'Sovtimer slutförd - uppspelning pausad'; + @override + String get playButton => 'Spela'; + @override + String get pauseButton => 'Pausa'; + @override + String seekBackwardButton({required Object seconds}) => + 'Spola bakåt ${seconds} sekunder'; + @override + String seekForwardButton({required Object seconds}) => + 'Spola framåt ${seconds} sekunder'; + @override + String get previousButton => 'Föregående avsnitt'; + @override + String get nextButton => 'Nästa avsnitt'; + @override + String get previousChapterButton => 'Föregående kapitel'; + @override + String get nextChapterButton => 'Nästa kapitel'; + @override + String get volumeButton => 'Volym'; + @override + String get muteButton => 'Tysta'; + @override + String get unmuteButton => 'Slå på ljud'; + @override + String get settingsButton => 'Videoinställningar'; + @override + String get audioTrackButton => 'Ljudspår'; + @override + String get subtitlesButton => 'Undertexter'; + @override + String get chaptersButton => 'Kapitel'; + @override + String get versionsButton => 'Videoversioner'; + @override + String get aspectRatioButton => 'Bildförhållande'; + @override + String get fullscreenButton => 'Aktivera helskärm'; + @override + String get exitFullscreenButton => 'Avsluta helskärm'; + @override + String get rotationLockButton => 'Rotationslås'; + @override + String get timelineSlider => 'Videotidslinje'; + @override + String get volumeSlider => 'Volymnivå'; + @override + String get backButton => 'Tillbaka'; } // Path: userStatus class _StringsUserStatusSv implements _StringsUserStatusEn { - _StringsUserStatusSv._(this._root); + _StringsUserStatusSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get admin => 'Admin'; - @override String get restricted => 'Begränsad'; - @override String get protected => 'Skyddad'; - @override String get current => 'NUVARANDE'; + // Translations + @override + String get admin => 'Admin'; + @override + String get restricted => 'Begränsad'; + @override + String get protected => 'Skyddad'; + @override + String get current => 'NUVARANDE'; } // Path: messages class _StringsMessagesSv implements _StringsMessagesEn { - _StringsMessagesSv._(this._root); + _StringsMessagesSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get markedAsWatched => 'Markerad som sedd'; - @override String get markedAsUnwatched => 'Markerad som osedd'; - @override String get removedFromContinueWatching => 'Borttagen från Fortsätt titta'; - @override String errorLoading({required Object error}) => 'Fel: ${error}'; - @override String get fileInfoNotAvailable => 'Filinformation inte tillgänglig'; - @override String errorLoadingFileInfo({required Object error}) => 'Fel vid laddning av filinformation: ${error}'; - @override String get errorLoadingSeries => 'Fel vid laddning av serie'; - @override String get errorLoadingSeason => 'Fel vid laddning av säsong'; - @override String get musicNotSupported => 'Musikuppspelning stöds inte ännu'; - @override String get logsCleared => 'Loggar rensade'; - @override String get logsCopied => 'Loggar kopierade till urklipp'; - @override String get noLogsAvailable => 'Inga loggar tillgängliga'; - @override String libraryScanning({required Object title}) => 'Skannar "${title}"...'; - @override String libraryScanStarted({required Object title}) => 'Biblioteksskanning startad för "${title}"'; - @override String libraryScanFailed({required Object error}) => 'Misslyckades att skanna bibliotek: ${error}'; - @override String metadataRefreshing({required Object title}) => 'Uppdaterar metadata för "${title}"...'; - @override String metadataRefreshStarted({required Object title}) => 'Metadata-uppdatering startad för "${title}"'; - @override String metadataRefreshFailed({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}'; - @override String get noPlexToken => 'Ingen Plex-token hittad. Vänligen logga in igen.'; - @override String get logoutConfirm => 'Är du säker på att du vill logga ut?'; - @override String get noSeasonsFound => 'Inga säsonger hittades'; - @override String get noEpisodesFound => 'Inga avsnitt hittades i första säsongen'; - @override String get noEpisodesFoundGeneral => 'Inga avsnitt hittades'; - @override String get noResultsFound => 'Inga resultat hittades'; - @override String sleepTimerSet({required Object label}) => 'Sovtimer inställd för ${label}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Misslyckades att byta till ${displayName}'; - @override String get noItemsAvailable => 'Inga objekt tillgängliga'; - @override String get failedToCreatePlayQueue => 'Det gick inte att skapa uppspelningskö'; - @override String get failedToCreatePlayQueueNoItems => 'Det gick inte att skapa uppspelningskö – inga objekt'; - @override String failedPlayback({required Object action, required Object error}) => 'Kunde inte ${action}: ${error}'; + // Translations + @override + String get markedAsWatched => 'Markerad som sedd'; + @override + String get markedAsUnwatched => 'Markerad som osedd'; + @override + String get markedAsWatchedOffline => + 'Markerad som sedd (synkroniseras när online)'; + @override + String get markedAsUnwatchedOffline => + 'Markerad som osedd (synkroniseras när online)'; + @override + String get removedFromContinueWatching => 'Borttagen från Fortsätt titta'; + @override + String errorLoading({required Object error}) => 'Fel: ${error}'; + @override + String get fileInfoNotAvailable => 'Filinformation inte tillgänglig'; + @override + String errorLoadingFileInfo({required Object error}) => + 'Fel vid laddning av filinformation: ${error}'; + @override + String get errorLoadingSeries => 'Fel vid laddning av serie'; + @override + String get errorLoadingSeason => 'Fel vid laddning av säsong'; + @override + String get musicNotSupported => 'Musikuppspelning stöds inte ännu'; + @override + String get logsCleared => 'Loggar rensade'; + @override + String get logsCopied => 'Loggar kopierade till urklipp'; + @override + String get noLogsAvailable => 'Inga loggar tillgängliga'; + @override + String libraryScanning({required Object title}) => 'Skannar "${title}"...'; + @override + String libraryScanStarted({required Object title}) => + 'Biblioteksskanning startad för "${title}"'; + @override + String libraryScanFailed({required Object error}) => + 'Misslyckades att skanna bibliotek: ${error}'; + @override + String metadataRefreshing({required Object title}) => + 'Uppdaterar metadata för "${title}"...'; + @override + String metadataRefreshStarted({required Object title}) => + 'Metadata-uppdatering startad för "${title}"'; + @override + String metadataRefreshFailed({required Object error}) => + 'Misslyckades att uppdatera metadata: ${error}'; + @override + String get noPlexToken => 'Ingen Plex-token hittad. Vänligen logga in igen.'; + @override + String get logoutConfirm => 'Är du säker på att du vill logga ut?'; + @override + String get noSeasonsFound => 'Inga säsonger hittades'; + @override + String get noEpisodesFound => 'Inga avsnitt hittades i första säsongen'; + @override + String get noEpisodesFoundGeneral => 'Inga avsnitt hittades'; + @override + String get noResultsFound => 'Inga resultat hittades'; + @override + String sleepTimerSet({required Object label}) => + 'Sovtimer inställd för ${label}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Misslyckades att byta till ${displayName}'; + @override + String get noItemsAvailable => 'Inga objekt tillgängliga'; + @override + String get failedToCreatePlayQueue => + 'Det gick inte att skapa uppspelningskö'; + @override + String get failedToCreatePlayQueueNoItems => + 'Det gick inte att skapa uppspelningskö – inga objekt'; + @override + String failedPlayback({required Object action, required Object error}) => + 'Kunde inte ${action}: ${error}'; + @override + String get youAreOffline => 'Du är offline'; + @override + String get offlineFeatureUnavailable => + 'Denna funktion kräver en internetanslutning. Dina nedladdningar och inställningar är fortfarande tillgängliga.'; } // Path: profile class _StringsProfileSv implements _StringsProfileEn { - _StringsProfileSv._(this._root); + _StringsProfileSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get noUsersAvailable => 'Inga användare tillgängliga'; + // Translations + @override + String get noUsersAvailable => 'Inga användare tillgängliga'; } // Path: subtitlingStyling class _StringsSubtitlingStylingSv implements _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingSv._(this._root); + _StringsSubtitlingStylingSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get stylingOptions => 'Stilalternativ'; - @override String get fontSize => 'Teckenstorlek'; - @override String get textColor => 'Textfärg'; - @override String get borderSize => 'Kantstorlek'; - @override String get borderColor => 'Kantfärg'; - @override String get backgroundOpacity => 'Bakgrundsopacitet'; - @override String get backgroundColor => 'Bakgrundsfärg'; + // Translations + @override + String get stylingOptions => 'Stilalternativ'; + @override + String get fontSize => 'Teckenstorlek'; + @override + String get textColor => 'Textfärg'; + @override + String get borderSize => 'Kantstorlek'; + @override + String get borderColor => 'Kantfärg'; + @override + String get backgroundOpacity => 'Bakgrundsopacitet'; + @override + String get backgroundColor => 'Bakgrundsfärg'; } // Path: dialog class _StringsDialogSv implements _StringsDialogEn { - _StringsDialogSv._(this._root); + _StringsDialogSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get confirmAction => 'Bekräfta åtgärd'; - @override String get areYouSure => 'Är du säker på att du vill utföra denna åtgärd?'; - @override String get cancel => 'Avbryt'; - @override String get playNow => 'Spela nu'; + // Translations + @override + String get confirmAction => 'Bekräfta åtgärd'; + @override + String get areYouSure => 'Är du säker på att du vill utföra denna åtgärd?'; + @override + String get cancel => 'Avbryt'; + @override + String get playNow => 'Spela nu'; } // Path: discover class _StringsDiscoverSv implements _StringsDiscoverEn { - _StringsDiscoverSv._(this._root); + _StringsDiscoverSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Upptäck'; - @override String get switchProfile => 'Byt profil'; - @override String get switchServer => 'Byt server'; - @override String get logout => 'Logga ut'; - @override String get noContentAvailable => 'Inget innehåll tillgängligt'; - @override String get addMediaToLibraries => 'Lägg till media till dina bibliotek'; - @override String get continueWatching => 'Fortsätt titta'; - @override String get recentlyAdded => 'Nyligen tillagda'; - @override String get play => 'Spela'; - @override String get resume => 'Återuppta'; - @override String playEpisode({required Object season, required Object episode}) => 'Spela S${season}, E${episode}'; - @override String resumeEpisode({required Object season, required Object episode}) => 'Återuppta S${season}, E${episode}'; - @override String get pause => 'Pausa'; - @override String get overview => 'Översikt'; - @override String get cast => 'Rollbesättning'; - @override String get seasons => 'Säsonger'; - @override String get studio => 'Studio'; - @override String get rating => 'Åldersgräns'; - @override String get watched => 'Tittad'; - @override String episodeCount({required Object count}) => '${count} avsnitt'; - @override String watchedProgress({required Object watched, required Object total}) => '${watched}/${total} sedda'; - @override String get movie => 'Film'; - @override String get tvShow => 'TV-serie'; - @override String minutesLeft({required Object minutes}) => '${minutes} min kvar'; + // Translations + @override + String get title => 'Upptäck'; + @override + String get switchProfile => 'Byt profil'; + @override + String get switchServer => 'Byt server'; + @override + String get logout => 'Logga ut'; + @override + String get noContentAvailable => 'Inget innehåll tillgängligt'; + @override + String get addMediaToLibraries => 'Lägg till media till dina bibliotek'; + @override + String get continueWatching => 'Fortsätt titta'; + @override + String get recentlyAdded => 'Nyligen tillagda'; + @override + String get play => 'Spela'; + @override + String get resume => 'Återuppta'; + @override + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String get pause => 'Pausa'; + @override + String get overview => 'Översikt'; + @override + String get cast => 'Rollbesättning'; + @override + String get seasons => 'Säsonger'; + @override + String get studio => 'Studio'; + @override + String get rating => 'Åldersgräns'; + @override + String get watched => 'Tittad'; + @override + String episodeCount({required Object count}) => '${count} avsnitt'; + @override + String watchedProgress({required Object watched, required Object total}) => + '${watched}/${total} sedda'; + @override + String get movie => 'Film'; + @override + String get tvShow => 'TV-serie'; + @override + String minutesLeft({required Object minutes}) => '${minutes} min kvar'; } // Path: errors class _StringsErrorsSv implements _StringsErrorsEn { - _StringsErrorsSv._(this._root); + _StringsErrorsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String searchFailed({required Object error}) => 'Sökning misslyckades: ${error}'; - @override String connectionTimeout({required Object context}) => 'Anslutnings-timeout vid laddning ${context}'; - @override String get connectionFailed => 'Kan inte ansluta till Plex-server'; - @override String failedToLoad({required Object context, required Object error}) => 'Misslyckades att ladda ${context}: ${error}'; - @override String get noClientAvailable => 'Ingen klient tillgänglig'; - @override String authenticationFailed({required Object error}) => 'Autentisering misslyckades: ${error}'; - @override String get couldNotLaunchUrl => 'Kunde inte öppna autentiserings-URL'; - @override String get pleaseEnterToken => 'Vänligen ange en token'; - @override String get invalidToken => 'Ogiltig token'; - @override String failedToVerifyToken({required Object error}) => 'Misslyckades att verifiera token: ${error}'; - @override String failedToSwitchProfile({required Object displayName}) => 'Misslyckades att byta till ${displayName}'; - @override String get connectionFailedGeneric => 'Anslutning misslyckades'; + // Translations + @override + String searchFailed({required Object error}) => + 'Sökning misslyckades: ${error}'; + @override + String connectionTimeout({required Object context}) => + 'Anslutnings-timeout vid laddning ${context}'; + @override + String get connectionFailed => 'Kan inte ansluta till Plex-server'; + @override + String failedToLoad({required Object context, required Object error}) => + 'Misslyckades att ladda ${context}: ${error}'; + @override + String get noClientAvailable => 'Ingen klient tillgänglig'; + @override + String authenticationFailed({required Object error}) => + 'Autentisering misslyckades: ${error}'; + @override + String get couldNotLaunchUrl => 'Kunde inte öppna autentiserings-URL'; + @override + String get pleaseEnterToken => 'Vänligen ange en token'; + @override + String get invalidToken => 'Ogiltig token'; + @override + String failedToVerifyToken({required Object error}) => + 'Misslyckades att verifiera token: ${error}'; + @override + String failedToSwitchProfile({required Object displayName}) => + 'Misslyckades att byta till ${displayName}'; + @override + String get connectionFailedGeneric => 'Anslutning misslyckades'; } // Path: libraries class _StringsLibrariesSv implements _StringsLibrariesEn { - _StringsLibrariesSv._(this._root); + _StringsLibrariesSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Bibliotek'; - @override String get scanLibraryFiles => 'Skanna biblioteksfiler'; - @override String get scanLibrary => 'Skanna bibliotek'; - @override String get analyze => 'Analysera'; - @override String get analyzeLibrary => 'Analysera bibliotek'; - @override String get refreshMetadata => 'Uppdatera metadata'; - @override String get emptyTrash => 'Töm papperskorg'; - @override String emptyingTrash({required Object title}) => 'Tömmer papperskorg för "${title}"...'; - @override String trashEmptied({required Object title}) => 'Papperskorg tömd för "${title}"'; - @override String failedToEmptyTrash({required Object error}) => 'Misslyckades att tömma papperskorg: ${error}'; - @override String analyzing({required Object title}) => 'Analyserar "${title}"...'; - @override String analysisStarted({required Object title}) => 'Analys startad för "${title}"'; - @override String failedToAnalyze({required Object error}) => 'Misslyckades att analysera bibliotek: ${error}'; - @override String get noLibrariesFound => 'Inga bibliotek hittades'; - @override String get thisLibraryIsEmpty => 'Detta bibliotek är tomt'; - @override String get all => 'Alla'; - @override String get clearAll => 'Rensa alla'; - @override String scanLibraryConfirm({required Object title}) => 'Är du säker på att du vill skanna "${title}"?'; - @override String analyzeLibraryConfirm({required Object title}) => 'Är du säker på att du vill analysera "${title}"?'; - @override String refreshMetadataConfirm({required Object title}) => 'Är du säker på att du vill uppdatera metadata för "${title}"?'; - @override String emptyTrashConfirm({required Object title}) => 'Är du säker på att du vill tömma papperskorgen för "${title}"?'; - @override String get manageLibraries => 'Hantera bibliotek'; - @override String get sort => 'Sortera'; - @override String get sortBy => 'Sortera efter'; - @override String get filters => 'Filter'; - @override String loadingLibraryWithCount({required Object count}) => 'Laddar bibliotek... (${count} objekt laddade)'; - @override String get confirmActionMessage => 'Är du säker på att du vill utföra denna åtgärd?'; - @override String get showLibrary => 'Visa bibliotek'; - @override String get hideLibrary => 'Dölj bibliotek'; - @override String get libraryOptions => 'Biblioteksalternativ'; - @override String get content => 'bibliotekets innehåll'; - @override String get selectLibrary => 'Välj bibliotek'; - @override String filtersWithCount({required Object count}) => 'Filter (${count})'; - @override String get noRecommendations => 'Inga rekommendationer tillgängliga'; - @override String get noCollections => 'Inga samlingar i det här biblioteket'; - @override String get noFoldersFound => 'Inga mappar hittades'; - @override String get folders => 'mappar'; - @override late final _StringsLibrariesTabsSv tabs = _StringsLibrariesTabsSv._(_root); - @override late final _StringsLibrariesGroupingsSv groupings = _StringsLibrariesGroupingsSv._(_root); + // Translations + @override + String get title => 'Bibliotek'; + @override + String get scanLibraryFiles => 'Skanna biblioteksfiler'; + @override + String get scanLibrary => 'Skanna bibliotek'; + @override + String get analyze => 'Analysera'; + @override + String get analyzeLibrary => 'Analysera bibliotek'; + @override + String get refreshMetadata => 'Uppdatera metadata'; + @override + String get emptyTrash => 'Töm papperskorg'; + @override + String emptyingTrash({required Object title}) => + 'Tömmer papperskorg för "${title}"...'; + @override + String trashEmptied({required Object title}) => + 'Papperskorg tömd för "${title}"'; + @override + String failedToEmptyTrash({required Object error}) => + 'Misslyckades att tömma papperskorg: ${error}'; + @override + String analyzing({required Object title}) => 'Analyserar "${title}"...'; + @override + String analysisStarted({required Object title}) => + 'Analys startad för "${title}"'; + @override + String failedToAnalyze({required Object error}) => + 'Misslyckades att analysera bibliotek: ${error}'; + @override + String get noLibrariesFound => 'Inga bibliotek hittades'; + @override + String get thisLibraryIsEmpty => 'Detta bibliotek är tomt'; + @override + String get all => 'Alla'; + @override + String get clearAll => 'Rensa alla'; + @override + String scanLibraryConfirm({required Object title}) => + 'Är du säker på att du vill skanna "${title}"?'; + @override + String analyzeLibraryConfirm({required Object title}) => + 'Är du säker på att du vill analysera "${title}"?'; + @override + String refreshMetadataConfirm({required Object title}) => + 'Är du säker på att du vill uppdatera metadata för "${title}"?'; + @override + String emptyTrashConfirm({required Object title}) => + 'Är du säker på att du vill tömma papperskorgen för "${title}"?'; + @override + String get manageLibraries => 'Hantera bibliotek'; + @override + String get sort => 'Sortera'; + @override + String get sortBy => 'Sortera efter'; + @override + String get filters => 'Filter'; + @override + String loadingLibraryWithCount({required Object count}) => + 'Laddar bibliotek... (${count} objekt laddade)'; + @override + String get confirmActionMessage => + 'Är du säker på att du vill utföra denna åtgärd?'; + @override + String get showLibrary => 'Visa bibliotek'; + @override + String get hideLibrary => 'Dölj bibliotek'; + @override + String get libraryOptions => 'Biblioteksalternativ'; + @override + String get content => 'bibliotekets innehåll'; + @override + String get selectLibrary => 'Välj bibliotek'; + @override + String filtersWithCount({required Object count}) => 'Filter (${count})'; + @override + String get noRecommendations => 'Inga rekommendationer tillgängliga'; + @override + String get noCollections => 'Inga samlingar i det här biblioteket'; + @override + String get noFoldersFound => 'Inga mappar hittades'; + @override + String get folders => 'mappar'; + @override + late final _StringsLibrariesTabsSv tabs = _StringsLibrariesTabsSv._(_root); + @override + late final _StringsLibrariesGroupingsSv groupings = + _StringsLibrariesGroupingsSv._(_root); } // Path: about class _StringsAboutSv implements _StringsAboutEn { - _StringsAboutSv._(this._root); + _StringsAboutSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Om'; - @override String get openSourceLicenses => 'Öppen källkod-licenser'; - @override String versionLabel({required Object version}) => 'Version ${version}'; - @override String get appDescription => 'En vacker Plex-klient för Flutter'; - @override String get viewLicensesDescription => 'Visa licenser för tredjepartsbibliotek'; + // Translations + @override + String get title => 'Om'; + @override + String get openSourceLicenses => 'Öppen källkod-licenser'; + @override + String versionLabel({required Object version}) => 'Version ${version}'; + @override + String get appDescription => 'En vacker Plex-klient för Flutter'; + @override + String get viewLicensesDescription => + 'Visa licenser för tredjepartsbibliotek'; } // Path: serverSelection class _StringsServerSelectionSv implements _StringsServerSelectionEn { - _StringsServerSelectionSv._(this._root); + _StringsServerSelectionSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get connectingToServer => 'Ansluter till server...'; - @override String get connectingToAllServers => 'Ansluter till alla servrar...'; - @override String get connectToAllServers => 'Anslut till alla servrar'; - @override String get allServerConnectionsFailed => 'Misslyckades att ansluta till servrar. Kontrollera ditt nätverk och försök igen.'; - @override String foundServers({required Object count}) => 'Hittade ${count} server(ar)'; - @override String get orSelectIndividually => 'Eller välj en server individuellt:'; - @override String get serverDebugCopied => 'Server-felsökningsdata kopierad till urklipp'; - @override String get copyDebugData => 'Kopiera felsökningsdata'; - @override String get noServersFound => 'Inga servrar hittades'; - @override String noServersFoundForAccount({required Object username, required Object email}) => 'Inga servrar hittades för ${username} (${email})'; - @override String malformedServerData({required Object count}) => 'Hittade ${count} server(ar) med felformaterad data. Inga giltiga servrar tillgängliga.'; - @override String get incompleteServerInfo => 'Vissa servrar har ofullständig information och hoppades över. Vänligen kontrollera ditt Plex.tv-konto.'; - @override String get incompleteConnectionInfo => 'Server-anslutningsinformation är ofullständig. Försök igen.'; - @override String malformedServerInfo({required Object message}) => 'Serverinformation är felformaterad: ${message}'; - @override String get networkConnectionFailed => 'Nätverksanslutning misslyckades. Kontrollera din internetanslutning och försök igen.'; - @override String get authenticationFailed => 'Autentisering misslyckades. Logga in igen.'; - @override String get plexServiceUnavailable => 'Plex-tjänst otillgänglig. Försök igen senare.'; - @override String failedToLoadServers({required Object error}) => 'Misslyckades att ladda servrar: ${error}'; + // Translations + @override + String get connectingToServer => 'Ansluter till server...'; + @override + String get connectingToAllServers => 'Ansluter till alla servrar...'; + @override + String get connectToAllServers => 'Anslut till alla servrar'; + @override + String get allServerConnectionsFailed => + 'Misslyckades att ansluta till servrar. Kontrollera ditt nätverk och försök igen.'; + @override + String foundServers({required Object count}) => 'Hittade ${count} server(ar)'; + @override + String get orSelectIndividually => 'Eller välj en server individuellt:'; + @override + String get serverDebugCopied => + 'Server-felsökningsdata kopierad till urklipp'; + @override + String get copyDebugData => 'Kopiera felsökningsdata'; + @override + String get noServersFound => 'Inga servrar hittades'; + @override + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => 'Inga servrar hittades för ${username} (${email})'; + @override + String malformedServerData({required Object count}) => + 'Hittade ${count} server(ar) med felformaterad data. Inga giltiga servrar tillgängliga.'; + @override + String get incompleteServerInfo => + 'Vissa servrar har ofullständig information och hoppades över. Vänligen kontrollera ditt Plex.tv-konto.'; + @override + String get incompleteConnectionInfo => + 'Server-anslutningsinformation är ofullständig. Försök igen.'; + @override + String malformedServerInfo({required Object message}) => + 'Serverinformation är felformaterad: ${message}'; + @override + String get networkConnectionFailed => + 'Nätverksanslutning misslyckades. Kontrollera din internetanslutning och försök igen.'; + @override + String get authenticationFailed => + 'Autentisering misslyckades. Logga in igen.'; + @override + String get plexServiceUnavailable => + 'Plex-tjänst otillgänglig. Försök igen senare.'; + @override + String failedToLoadServers({required Object error}) => + 'Misslyckades att ladda servrar: ${error}'; } // Path: hubDetail class _StringsHubDetailSv implements _StringsHubDetailEn { - _StringsHubDetailSv._(this._root); + _StringsHubDetailSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Titel'; - @override String get releaseYear => 'Utgivningsår'; - @override String get dateAdded => 'Datum tillagd'; - @override String get rating => 'Betyg'; - @override String get noItemsFound => 'Inga objekt hittades'; + // Translations + @override + String get title => 'Titel'; + @override + String get releaseYear => 'Utgivningsår'; + @override + String get dateAdded => 'Datum tillagd'; + @override + String get rating => 'Betyg'; + @override + String get noItemsFound => 'Inga objekt hittades'; } // Path: logs class _StringsLogsSv implements _StringsLogsEn { - _StringsLogsSv._(this._root); + _StringsLogsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Loggar'; - @override String get clearLogs => 'Rensa loggar'; - @override String get copyLogs => 'Kopiera loggar'; - @override String get exportLogs => 'Exportera loggar'; - @override String get noLogsToShow => 'Inga loggar att visa'; - @override String get error => 'Fel:'; - @override String get stackTrace => 'Stack trace:'; + // Translations + @override + String get title => 'Loggar'; + @override + String get clearLogs => 'Rensa loggar'; + @override + String get copyLogs => 'Kopiera loggar'; + @override + String get exportLogs => 'Exportera loggar'; + @override + String get noLogsToShow => 'Inga loggar att visa'; + @override + String get error => 'Fel:'; + @override + String get stackTrace => 'Stack trace:'; } // Path: licenses class _StringsLicensesSv implements _StringsLicensesEn { - _StringsLicensesSv._(this._root); + _StringsLicensesSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get relatedPackages => 'Relaterade paket'; - @override String get license => 'Licens'; - @override String licenseNumber({required Object number}) => 'Licens ${number}'; - @override String licensesCount({required Object count}) => '${count} licenser'; + // Translations + @override + String get relatedPackages => 'Relaterade paket'; + @override + String get license => 'Licens'; + @override + String licenseNumber({required Object number}) => 'Licens ${number}'; + @override + String licensesCount({required Object count}) => '${count} licenser'; } // Path: navigation class _StringsNavigationSv implements _StringsNavigationEn { - _StringsNavigationSv._(this._root); + _StringsNavigationSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get home => 'Hem'; - @override String get search => 'Sök'; - @override String get libraries => 'Bibliotek'; - @override String get settings => 'Inställningar'; + // Translations + @override + String get home => 'Hem'; + @override + String get search => 'Sök'; + @override + String get libraries => 'Bibliotek'; + @override + String get settings => 'Inställningar'; + @override + String get downloads => 'Nedladdningar'; +} + +// Path: downloads +class _StringsDownloadsSv implements _StringsDownloadsEn { + _StringsDownloadsSv._(this._root); + + @override + final _StringsSv _root; // ignore: unused_field + + // Translations + @override + String get title => 'Nedladdningar'; + @override + String get manage => 'Hantera'; + @override + String get tvShows => 'TV-serier'; + @override + String get movies => 'Filmer'; + @override + String get queue => 'Kö'; + @override + String get noDownloads => 'Inga nedladdningar ännu'; + @override + String get noDownloadsDescription => + 'Nedladdat innehåll visas här för offline-visning'; + @override + String get browseContent => 'Bläddra i innehåll'; + @override + String get downloading => 'Laddar ner'; + @override + String get downloaded => 'Nedladdad'; + @override + String get paused => 'Pausad'; + @override + String get failed => 'Misslyckades'; + @override + String get queued => 'I kö'; + @override + String get downloadNow => 'Ladda ner'; + @override + String get deleteDownload => 'Ta bort nedladdning'; + @override + String get cancelDownload => 'Avbryt nedladdning'; + @override + String get pauseDownload => 'Pausa nedladdning'; + @override + String get resumeDownload => 'Återuppta nedladdning'; + @override + String get retryDownload => 'Försök igen'; + @override + String get downloadComplete => 'Nedladdning klar'; + @override + String get downloadFailed => 'Nedladdning misslyckades'; + @override + String get downloadCancelled => 'Nedladdning avbruten'; + @override + String get downloadQueued => 'Nedladdning köad'; + @override + String episodesQueued({required Object count}) => + '${count} avsnitt köade för nedladdning'; + @override + String get downloadDeleted => 'Nedladdning borttagen'; + @override + String get deleteConfirmTitle => 'Ta bort nedladdning?'; + @override + String deleteConfirm({required Object title}) => + 'Är du säker på att du vill ta bort "${title}"? Den nedladdade filen kommer att tas bort från din enhet.'; + @override + String get deleteConfirmMessage => + 'Den nedladdade filen kommer att tas bort från din enhet.'; + @override + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => 'Tar bort ${title}... (${current} av ${total})'; + @override + String deletingSimple({required Object title}) => 'Tar bort ${title}...'; + @override + String get storageUsed => 'Lagring använd'; + @override + String get availableOffline => 'Tillgänglig offline'; + @override + String get downloadingSeason => 'Laddar ner säsong'; + @override + String get downloadingEpisode => 'Laddar ner avsnitt'; +} + +// Path: offline +class _StringsOfflineSv implements _StringsOfflineEn { + _StringsOfflineSv._(this._root); + + @override + final _StringsSv _root; // ignore: unused_field + + // Translations + @override + String get youAreOffline => 'Du är offline'; + @override + String get offlineMode => 'Offline-läge'; + @override + String get backOnline => 'Tillbaka online'; + @override + String get showingDownloadedContent => 'Visar endast nedladdat innehåll'; + @override + String get noInternetConnection => 'Ingen internetanslutning'; + @override + String get serversUnreachable => 'Plex-servrar kan inte nås'; + @override + String get connectToDownload => + 'Anslut till internet för att ladda ner innehåll'; } // Path: playlists class _StringsPlaylistsSv implements _StringsPlaylistsEn { - _StringsPlaylistsSv._(this._root); + _StringsPlaylistsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Spellistor'; - @override String get noPlaylists => 'Inga spellistor hittades'; - @override String get create => 'Skapa spellista'; - @override String get newPlaylist => 'Ny spellista'; - @override String get playlistName => 'Spellistans namn'; - @override String get enterPlaylistName => 'Ange spellistans namn'; - @override String get edit => 'Redigera spellista'; - @override String get delete => 'Ta bort spellista'; - @override String get addTo => 'Lägg till i spellista'; - @override String get addItems => 'Lägg till objekt'; - @override String get removeItem => 'Ta bort från spellista'; - @override String get clearPlaylist => 'Rensa spellista'; - @override String get playAll => 'Spela alla'; - @override String get shuffle => 'Blanda'; - @override String get smartPlaylist => 'Smart spellista'; - @override String get regularPlaylist => 'Vanlig spellista'; - @override String itemCount({required Object count}) => '${count} objekt'; - @override String get oneItem => '1 objekt'; - @override String get emptyPlaylist => 'Denna spellista är tom'; - @override String get deleteConfirm => 'Ta bort spellista?'; - @override String deleteMessage({required Object name}) => 'Är du säker på att du vill ta bort "${name}"?'; - @override String get created => 'Spellista skapad'; - @override String get updated => 'Spellista uppdaterad'; - @override String get deleted => 'Spellista borttagen'; - @override String get itemAdded => 'Tillagd i spellista'; - @override String get itemRemoved => 'Borttagen från spellista'; - @override String get selectPlaylist => 'Välj spellista'; - @override String get createNewPlaylist => 'Skapa ny spellista'; - @override String get errorCreating => 'Det gick inte att skapa spellista'; - @override String get errorDeleting => 'Det gick inte att ta bort spellista'; - @override String get errorLoading => 'Det gick inte att ladda spellistor'; - @override String get errorAdding => 'Det gick inte att lägga till i spellista'; - @override String get errorReordering => 'Det gick inte att omordna spellisteobjekt'; - @override String get errorRemoving => 'Det gick inte att ta bort från spellista'; - @override String get playlist => 'Spellista'; + // Translations + @override + String get title => 'Spellistor'; + @override + String get noPlaylists => 'Inga spellistor hittades'; + @override + String get create => 'Skapa spellista'; + @override + String get newPlaylist => 'Ny spellista'; + @override + String get playlistName => 'Spellistans namn'; + @override + String get enterPlaylistName => 'Ange spellistans namn'; + @override + String get edit => 'Redigera spellista'; + @override + String get delete => 'Ta bort spellista'; + @override + String get addTo => 'Lägg till i spellista'; + @override + String get addItems => 'Lägg till objekt'; + @override + String get removeItem => 'Ta bort från spellista'; + @override + String get clearPlaylist => 'Rensa spellista'; + @override + String get playAll => 'Spela alla'; + @override + String get shuffle => 'Blanda'; + @override + String get smartPlaylist => 'Smart spellista'; + @override + String get regularPlaylist => 'Vanlig spellista'; + @override + String itemCount({required Object count}) => '${count} objekt'; + @override + String get oneItem => '1 objekt'; + @override + String get emptyPlaylist => 'Denna spellista är tom'; + @override + String get deleteConfirm => 'Ta bort spellista?'; + @override + String deleteMessage({required Object name}) => + 'Är du säker på att du vill ta bort "${name}"?'; + @override + String get created => 'Spellista skapad'; + @override + String get updated => 'Spellista uppdaterad'; + @override + String get deleted => 'Spellista borttagen'; + @override + String get itemAdded => 'Tillagd i spellista'; + @override + String get itemRemoved => 'Borttagen från spellista'; + @override + String get selectPlaylist => 'Välj spellista'; + @override + String get createNewPlaylist => 'Skapa ny spellista'; + @override + String get errorCreating => 'Det gick inte att skapa spellista'; + @override + String get errorDeleting => 'Det gick inte att ta bort spellista'; + @override + String get errorLoading => 'Det gick inte att ladda spellistor'; + @override + String get errorAdding => 'Det gick inte att lägga till i spellista'; + @override + String get errorReordering => 'Det gick inte att omordna spellisteobjekt'; + @override + String get errorRemoving => 'Det gick inte att ta bort från spellista'; + @override + String get playlist => 'Spellista'; } // Path: collections class _StringsCollectionsSv implements _StringsCollectionsEn { - _StringsCollectionsSv._(this._root); + _StringsCollectionsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get title => 'Samlingar'; - @override String get collection => 'Samling'; - @override String get empty => 'Samlingen är tom'; - @override String get noItems => 'Inga objekt i den här samlingen'; - @override String get unknownLibrarySection => 'Kan inte ta bort: okänd bibliotekssektion'; - @override String get deleteCollection => 'Ta bort samling'; - @override String deleteConfirm({required Object title}) => 'Är du säker på att du vill ta bort "${title}"? Detta går inte att ångra.'; - @override String get deleted => 'Samling borttagen'; - @override String get deleteFailed => 'Det gick inte att ta bort samlingen'; - @override String deleteFailedWithError({required Object error}) => 'Det gick inte att ta bort samlingen: ${error}'; - @override String failedToLoadItems({required Object error}) => 'Det gick inte att läsa in samlingsobjekt: ${error}'; - @override String get addTo => 'Lägg till i samling'; - @override String get selectCollection => 'Välj samling'; - @override String get createNewCollection => 'Skapa ny samling'; - @override String get collectionName => 'Samlingsnamn'; - @override String get enterCollectionName => 'Ange samlingsnamn'; - @override String get addedToCollection => 'Tillagd i samling'; - @override String get errorAddingToCollection => 'Fel vid tillägg i samling'; - @override String get created => 'Samling skapad'; - @override String get removeFromCollection => 'Ta bort från samling'; - @override String removeFromCollectionConfirm({required Object title}) => 'Ta bort "${title}" från denna samling?'; - @override String get removedFromCollection => 'Borttagen från samling'; - @override String get removeFromCollectionFailed => 'Misslyckades med att ta bort från samling'; - @override String removeFromCollectionError({required Object error}) => 'Fel vid borttagning från samling: ${error}'; + // Translations + @override + String get title => 'Samlingar'; + @override + String get collection => 'Samling'; + @override + String get empty => 'Samlingen är tom'; + @override + String get noItems => 'Inga objekt i den här samlingen'; + @override + String get unknownLibrarySection => + 'Kan inte ta bort: okänd bibliotekssektion'; + @override + String get deleteCollection => 'Ta bort samling'; + @override + String deleteConfirm({required Object title}) => + 'Är du säker på att du vill ta bort "${title}"? Detta går inte att ångra.'; + @override + String get deleted => 'Samling borttagen'; + @override + String get deleteFailed => 'Det gick inte att ta bort samlingen'; + @override + String deleteFailedWithError({required Object error}) => + 'Det gick inte att ta bort samlingen: ${error}'; + @override + String failedToLoadItems({required Object error}) => + 'Det gick inte att läsa in samlingsobjekt: ${error}'; + @override + String get addTo => 'Lägg till i samling'; + @override + String get selectCollection => 'Välj samling'; + @override + String get createNewCollection => 'Skapa ny samling'; + @override + String get collectionName => 'Samlingsnamn'; + @override + String get enterCollectionName => 'Ange samlingsnamn'; + @override + String get addedToCollection => 'Tillagd i samling'; + @override + String get errorAddingToCollection => 'Fel vid tillägg i samling'; + @override + String get created => 'Samling skapad'; + @override + String get removeFromCollection => 'Ta bort från samling'; + @override + String removeFromCollectionConfirm({required Object title}) => + 'Ta bort "${title}" från denna samling?'; + @override + String get removedFromCollection => 'Borttagen från samling'; + @override + String get removeFromCollectionFailed => + 'Misslyckades med att ta bort från samling'; + @override + String removeFromCollectionError({required Object error}) => + 'Fel vid borttagning från samling: ${error}'; } // Path: libraries.tabs class _StringsLibrariesTabsSv implements _StringsLibrariesTabsEn { - _StringsLibrariesTabsSv._(this._root); + _StringsLibrariesTabsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get recommended => 'Rekommenderat'; - @override String get browse => 'Bläddra'; - @override String get collections => 'Samlingar'; - @override String get playlists => 'Spellistor'; + // Translations + @override + String get recommended => 'Rekommenderat'; + @override + String get browse => 'Bläddra'; + @override + String get collections => 'Samlingar'; + @override + String get playlists => 'Spellistor'; } // Path: libraries.groupings class _StringsLibrariesGroupingsSv implements _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsSv._(this._root); + _StringsLibrariesGroupingsSv._(this._root); - @override final _StringsSv _root; // ignore: unused_field + @override + final _StringsSv _root; // ignore: unused_field - // Translations - @override String get all => 'Alla'; - @override String get movies => 'Filmer'; - @override String get shows => 'Serier'; - @override String get seasons => 'Säsonger'; - @override String get episodes => 'Avsnitt'; - @override String get folders => 'Mappar'; + // Translations + @override + String get all => 'Alla'; + @override + String get movies => 'Filmer'; + @override + String get shows => 'Serier'; + @override + String get seasons => 'Säsonger'; + @override + String get episodes => 'Avsnitt'; + @override + String get folders => 'Mappar'; } // Path: class _StringsZh implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsZh.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.zh, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsZh.build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) : assert( + overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.', + ), + $meta = TranslationMetadata( + locale: AppLocale.zh, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } - /// Metadata for the translations of . - @override final TranslationMetadata $meta; + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); - @override late final _StringsZh _root = this; // ignore: unused_field + @override + late final _StringsZh _root = this; // ignore: unused_field - // Translations - @override late final _StringsAppZh app = _StringsAppZh._(_root); - @override late final _StringsAuthZh auth = _StringsAuthZh._(_root); - @override late final _StringsCommonZh common = _StringsCommonZh._(_root); - @override late final _StringsScreensZh screens = _StringsScreensZh._(_root); - @override late final _StringsUpdateZh update = _StringsUpdateZh._(_root); - @override late final _StringsSettingsZh settings = _StringsSettingsZh._(_root); - @override late final _StringsSearchZh search = _StringsSearchZh._(_root); - @override late final _StringsHotkeysZh hotkeys = _StringsHotkeysZh._(_root); - @override late final _StringsPinEntryZh pinEntry = _StringsPinEntryZh._(_root); - @override late final _StringsFileInfoZh fileInfo = _StringsFileInfoZh._(_root); - @override late final _StringsMediaMenuZh mediaMenu = _StringsMediaMenuZh._(_root); - @override late final _StringsAccessibilityZh accessibility = _StringsAccessibilityZh._(_root); - @override late final _StringsTooltipsZh tooltips = _StringsTooltipsZh._(_root); - @override late final _StringsVideoControlsZh videoControls = _StringsVideoControlsZh._(_root); - @override late final _StringsUserStatusZh userStatus = _StringsUserStatusZh._(_root); - @override late final _StringsMessagesZh messages = _StringsMessagesZh._(_root); - @override late final _StringsProfileZh profile = _StringsProfileZh._(_root); - @override late final _StringsSubtitlingStylingZh subtitlingStyling = _StringsSubtitlingStylingZh._(_root); - @override late final _StringsDialogZh dialog = _StringsDialogZh._(_root); - @override late final _StringsDiscoverZh discover = _StringsDiscoverZh._(_root); - @override late final _StringsErrorsZh errors = _StringsErrorsZh._(_root); - @override late final _StringsLibrariesZh libraries = _StringsLibrariesZh._(_root); - @override late final _StringsAboutZh about = _StringsAboutZh._(_root); - @override late final _StringsServerSelectionZh serverSelection = _StringsServerSelectionZh._(_root); - @override late final _StringsHubDetailZh hubDetail = _StringsHubDetailZh._(_root); - @override late final _StringsLogsZh logs = _StringsLogsZh._(_root); - @override late final _StringsLicensesZh licenses = _StringsLicensesZh._(_root); - @override late final _StringsNavigationZh navigation = _StringsNavigationZh._(_root); - @override late final _StringsPlaylistsZh playlists = _StringsPlaylistsZh._(_root); - @override late final _StringsCollectionsZh collections = _StringsCollectionsZh._(_root); + // Translations + @override + late final _StringsAppZh app = _StringsAppZh._(_root); + @override + late final _StringsAuthZh auth = _StringsAuthZh._(_root); + @override + late final _StringsCommonZh common = _StringsCommonZh._(_root); + @override + late final _StringsScreensZh screens = _StringsScreensZh._(_root); + @override + late final _StringsUpdateZh update = _StringsUpdateZh._(_root); + @override + late final _StringsSettingsZh settings = _StringsSettingsZh._(_root); + @override + late final _StringsSearchZh search = _StringsSearchZh._(_root); + @override + late final _StringsHotkeysZh hotkeys = _StringsHotkeysZh._(_root); + @override + late final _StringsPinEntryZh pinEntry = _StringsPinEntryZh._(_root); + @override + late final _StringsFileInfoZh fileInfo = _StringsFileInfoZh._(_root); + @override + late final _StringsMediaMenuZh mediaMenu = _StringsMediaMenuZh._(_root); + @override + late final _StringsAccessibilityZh accessibility = _StringsAccessibilityZh._( + _root, + ); + @override + late final _StringsTooltipsZh tooltips = _StringsTooltipsZh._(_root); + @override + late final _StringsVideoControlsZh videoControls = _StringsVideoControlsZh._( + _root, + ); + @override + late final _StringsUserStatusZh userStatus = _StringsUserStatusZh._(_root); + @override + late final _StringsMessagesZh messages = _StringsMessagesZh._(_root); + @override + late final _StringsProfileZh profile = _StringsProfileZh._(_root); + @override + late final _StringsSubtitlingStylingZh subtitlingStyling = + _StringsSubtitlingStylingZh._(_root); + @override + late final _StringsDialogZh dialog = _StringsDialogZh._(_root); + @override + late final _StringsDiscoverZh discover = _StringsDiscoverZh._(_root); + @override + late final _StringsErrorsZh errors = _StringsErrorsZh._(_root); + @override + late final _StringsLibrariesZh libraries = _StringsLibrariesZh._(_root); + @override + late final _StringsAboutZh about = _StringsAboutZh._(_root); + @override + late final _StringsServerSelectionZh serverSelection = + _StringsServerSelectionZh._(_root); + @override + late final _StringsHubDetailZh hubDetail = _StringsHubDetailZh._(_root); + @override + late final _StringsLogsZh logs = _StringsLogsZh._(_root); + @override + late final _StringsLicensesZh licenses = _StringsLicensesZh._(_root); + @override + late final _StringsNavigationZh navigation = _StringsNavigationZh._(_root); + @override + late final _StringsDownloadsZh downloads = _StringsDownloadsZh._(_root); + @override + late final _StringsOfflineZh offline = _StringsOfflineZh._(_root); + @override + late final _StringsPlaylistsZh playlists = _StringsPlaylistsZh._(_root); + @override + late final _StringsCollectionsZh collections = _StringsCollectionsZh._(_root); } // Path: app class _StringsAppZh implements _StringsAppEn { - _StringsAppZh._(this._root); + _StringsAppZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => 'Plezy'; - @override String get loading => '加载中...'; + // Translations + @override + String get title => 'Plezy'; + @override + String get loading => '加载中...'; } // Path: auth class _StringsAuthZh implements _StringsAuthEn { - _StringsAuthZh._(this._root); + _StringsAuthZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get signInWithPlex => '使用 Plex 登录'; - @override String get showQRCode => '显示二维码'; - @override String get cancel => '取消'; - @override String get authenticate => '验证'; - @override String get retry => '重试'; - @override String get debugEnterToken => '调试:输入 Plex Token'; - @override String get plexTokenLabel => 'Plex 授权令牌 (Auth Token)'; - @override String get plexTokenHint => '输入你的 Plex.tv 令牌'; - @override String get authenticationTimeout => '验证超时。请重试。'; - @override String get scanQRCodeInstruction => '请使用已登录 Plex 的设备扫描此二维码进行验证。'; - @override String get waitingForAuth => '等待验证中...\n请在你的浏览器中完成登录。'; + // Translations + @override + String get signInWithPlex => '使用 Plex 登录'; + @override + String get showQRCode => '显示二维码'; + @override + String get cancel => '取消'; + @override + String get authenticate => '验证'; + @override + String get retry => '重试'; + @override + String get debugEnterToken => '调试:输入 Plex Token'; + @override + String get plexTokenLabel => 'Plex 授权令牌 (Auth Token)'; + @override + String get plexTokenHint => '输入你的 Plex.tv 令牌'; + @override + String get authenticationTimeout => '验证超时。请重试。'; + @override + String get scanQRCodeInstruction => '请使用已登录 Plex 的设备扫描此二维码进行验证。'; + @override + String get waitingForAuth => '等待验证中...\n请在你的浏览器中完成登录。'; } // Path: common class _StringsCommonZh implements _StringsCommonEn { - _StringsCommonZh._(this._root); + _StringsCommonZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get cancel => '取消'; - @override String get save => '保存'; - @override String get close => '关闭'; - @override String get clear => '清除'; - @override String get reset => '重置'; - @override String get later => '稍后'; - @override String get submit => '提交'; - @override String get confirm => '确认'; - @override String get retry => '重试'; - @override String get playNow => '立即播放'; - @override String get logout => '登出'; - @override String get online => '在线'; - @override String get offline => '离线'; - @override String get owned => '拥有'; - @override String get shared => '共享'; - @override String get current => '当前'; - @override String get unknown => '未知'; - @override String get refresh => '刷新'; - @override String get yes => '是'; - @override String get no => '否'; - @override String get server => '服务器'; - @override String get delete => '删除'; - @override String get shuffle => '随机播放'; - @override String get addTo => '添加到...'; + // Translations + @override + String get cancel => '取消'; + @override + String get save => '保存'; + @override + String get close => '关闭'; + @override + String get clear => '清除'; + @override + String get reset => '重置'; + @override + String get later => '稍后'; + @override + String get submit => '提交'; + @override + String get confirm => '确认'; + @override + String get retry => '重试'; + @override + String get playNow => '立即播放'; + @override + String get logout => '登出'; + @override + String get online => '在线'; + @override + String get offline => '离线'; + @override + String get owned => '拥有'; + @override + String get shared => '共享'; + @override + String get current => '当前'; + @override + String get unknown => '未知'; + @override + String get refresh => '刷新'; + @override + String get yes => '是'; + @override + String get no => '否'; + @override + String get server => '服务器'; + @override + String get delete => '删除'; + @override + String get shuffle => '随机播放'; + @override + String get addTo => '添加到...'; } // Path: screens class _StringsScreensZh implements _StringsScreensEn { - _StringsScreensZh._(this._root); + _StringsScreensZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get licenses => '许可证'; - @override String get selectServer => '选择服务器'; - @override String get switchProfile => '切换配置文件'; - @override String get subtitleStyling => '字幕样式'; - @override String get search => '搜索'; - @override String get logs => '日志'; + // Translations + @override + String get licenses => '许可证'; + @override + String get selectServer => '选择服务器'; + @override + String get switchProfile => '切换配置文件'; + @override + String get subtitleStyling => '字幕样式'; + @override + String get search => '搜索'; + @override + String get logs => '日志'; } // Path: update class _StringsUpdateZh implements _StringsUpdateEn { - _StringsUpdateZh._(this._root); + _StringsUpdateZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get available => '有可用更新'; - @override String versionAvailable({required Object version}) => '版本 ${version} 已发布'; - @override String currentVersion({required Object version}) => '当前版本: ${version}'; - @override String get skipVersion => '跳过此版本'; - @override String get viewRelease => '查看发布详情'; - @override String get latestVersion => '已安装的版本是可用的最新版本'; - @override String get checkFailed => '无法检查更新'; + // Translations + @override + String get available => '有可用更新'; + @override + String versionAvailable({required Object version}) => '版本 ${version} 已发布'; + @override + String currentVersion({required Object version}) => '当前版本: ${version}'; + @override + String get skipVersion => '跳过此版本'; + @override + String get viewRelease => '查看发布详情'; + @override + String get latestVersion => '已安装的版本是可用的最新版本'; + @override + String get checkFailed => '无法检查更新'; } // Path: settings class _StringsSettingsZh implements _StringsSettingsEn { - _StringsSettingsZh._(this._root); + _StringsSettingsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '设置'; - @override String get language => '语言'; - @override String get theme => '主题'; - @override String get appearance => '外观'; - @override String get videoPlayback => '视频播放'; - @override String get shufflePlay => '随机播放'; - @override String get advanced => '高级'; - @override String get useSeasonPostersDescription => '为剧集显示季海报而非剧集海报'; - @override String get showHeroSectionDescription => '在主屏幕上显示精选内容轮播区'; - @override String get secondsLabel => '秒'; - @override String get minutesLabel => '分钟'; - @override String get secondsShort => 's'; - @override String get minutesShort => 'm'; - @override String durationHint({required Object min, required Object max}) => '输入时长 (${min}-${max})'; - @override String get systemTheme => '系统'; - @override String get systemThemeDescription => '跟随系统设置'; - @override String get lightTheme => '浅色'; - @override String get darkTheme => '深色'; - @override String get libraryDensity => '媒体库密度'; - @override String get compact => '紧凑'; - @override String get compactDescription => '卡片更小,显示更多项目'; - @override String get normal => '标准'; - @override String get normalDescription => '默认尺寸'; - @override String get comfortable => '舒适'; - @override String get comfortableDescription => '卡片更大,显示更少项目'; - @override String get viewMode => '视图模式'; - @override String get gridView => '网格视图'; - @override String get gridViewDescription => '以网格布局显示项目'; - @override String get listView => '列表视图'; - @override String get listViewDescription => '以列表布局显示项目'; - @override String get useSeasonPosters => '使用季海报'; - @override String get showHeroSection => '显示主要精选区'; - @override String get hardwareDecoding => '硬件解码'; - @override String get hardwareDecodingDescription => '如果可用,使用硬件加速'; - @override String get bufferSize => '缓冲区大小'; - @override String bufferSizeMB({required Object size}) => '${size}MB'; - @override String get subtitleStyling => '字幕样式'; - @override String get subtitleStylingDescription => '调整字幕外观'; - @override String get smallSkipDuration => '短跳过时长'; - @override String get largeSkipDuration => '长跳过时长'; - @override String secondsUnit({required Object seconds}) => '${seconds} 秒'; - @override String get defaultSleepTimer => '默认睡眠定时器'; - @override String minutesUnit({required Object minutes}) => '${minutes} 分钟'; - @override String get rememberTrackSelections => '记住每个剧集/电影的音轨选择'; - @override String get rememberTrackSelectionsDescription => '在播放过程中更改音轨时自动保存音频和字幕语言偏好'; - @override String get unwatchedOnly => '仅未观看'; - @override String get unwatchedOnlyDescription => '随机队列中仅包含未观看的剧集'; - @override String get shuffleOrderNavigation => '随机顺序导航'; - @override String get shuffleOrderNavigationDescription => '下一集/上一集按钮遵循随机播放顺序'; - @override String get loopShuffleQueue => '循环随机队列'; - @override String get loopShuffleQueueDescription => '在队列结束时重新开始'; - @override String get videoPlayerControls => '视频播放器控制'; - @override String get keyboardShortcuts => '键盘快捷键'; - @override String get keyboardShortcutsDescription => '自定义键盘快捷键'; - @override String get debugLogging => '调试日志'; - @override String get debugLoggingDescription => '启用详细日志记录以便故障排除'; - @override String get viewLogs => '查看日志'; - @override String get viewLogsDescription => '查看应用程序日志'; - @override String get clearCache => '清除缓存'; - @override String get clearCacheDescription => '这将清除所有缓存的图片和数据。清除缓存后,应用程序加载内容可能会变慢。'; - @override String get clearCacheSuccess => '缓存清除成功'; - @override String get resetSettings => '重置设置'; - @override String get resetSettingsDescription => '这会将所有设置重置为其默认值。此操作无法撤销。'; - @override String get resetSettingsSuccess => '设置重置成功'; - @override String get shortcutsReset => '快捷键已重置为默认值'; - @override String get about => '关于'; - @override String get aboutDescription => '应用程序信息和许可证'; - @override String get updates => '更新'; - @override String get updateAvailable => '有可用更新'; - @override String get checkForUpdates => '检查更新'; - @override String get validationErrorEnterNumber => '请输入一个有效的数字'; - @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 autoSkip => '自动跳过'; - @override String get autoSkipIntro => '自动跳过片头'; - @override String get autoSkipIntroDescription => '几秒钟后自动跳过片头标记'; - @override String get autoSkipCredits => '自动跳过片尾'; - @override String get autoSkipCreditsDescription => '自动跳过片尾并播放下一集'; - @override String get autoSkipDelay => '自动跳过延迟'; - @override String autoSkipDelayDescription({required Object seconds}) => '自动跳过前等待 ${seconds} 秒'; + // Translations + @override + String get title => '设置'; + @override + String get language => '语言'; + @override + String get theme => '主题'; + @override + String get appearance => '外观'; + @override + String get videoPlayback => '视频播放'; + @override + String get shufflePlay => '随机播放'; + @override + String get advanced => '高级'; + @override + String get useSeasonPostersDescription => '为剧集显示季海报而非剧集海报'; + @override + String get showHeroSectionDescription => '在主屏幕上显示精选内容轮播区'; + @override + String get secondsLabel => '秒'; + @override + String get minutesLabel => '分钟'; + @override + String get secondsShort => 's'; + @override + String get minutesShort => 'm'; + @override + String durationHint({required Object min, required Object max}) => + '输入时长 (${min}-${max})'; + @override + String get systemTheme => '系统'; + @override + String get systemThemeDescription => '跟随系统设置'; + @override + String get lightTheme => '浅色'; + @override + String get darkTheme => '深色'; + @override + String get libraryDensity => '媒体库密度'; + @override + String get compact => '紧凑'; + @override + String get compactDescription => '卡片更小,显示更多项目'; + @override + String get normal => '标准'; + @override + String get normalDescription => '默认尺寸'; + @override + String get comfortable => '舒适'; + @override + String get comfortableDescription => '卡片更大,显示更少项目'; + @override + String get viewMode => '视图模式'; + @override + String get gridView => '网格视图'; + @override + String get gridViewDescription => '以网格布局显示项目'; + @override + String get listView => '列表视图'; + @override + String get listViewDescription => '以列表布局显示项目'; + @override + String get useSeasonPosters => '使用季海报'; + @override + String get showHeroSection => '显示主要精选区'; + @override + String get hardwareDecoding => '硬件解码'; + @override + String get hardwareDecodingDescription => '如果可用,使用硬件加速'; + @override + String get bufferSize => '缓冲区大小'; + @override + String bufferSizeMB({required Object size}) => '${size}MB'; + @override + String get subtitleStyling => '字幕样式'; + @override + String get subtitleStylingDescription => '调整字幕外观'; + @override + String get smallSkipDuration => '短跳过时长'; + @override + String get largeSkipDuration => '长跳过时长'; + @override + String secondsUnit({required Object seconds}) => '${seconds} 秒'; + @override + String get defaultSleepTimer => '默认睡眠定时器'; + @override + String minutesUnit({required Object minutes}) => '${minutes} 分钟'; + @override + String get rememberTrackSelections => '记住每个剧集/电影的音轨选择'; + @override + String get rememberTrackSelectionsDescription => '在播放过程中更改音轨时自动保存音频和字幕语言偏好'; + @override + String get unwatchedOnly => '仅未观看'; + @override + String get unwatchedOnlyDescription => '随机队列中仅包含未观看的剧集'; + @override + String get shuffleOrderNavigation => '随机顺序导航'; + @override + String get shuffleOrderNavigationDescription => '下一集/上一集按钮遵循随机播放顺序'; + @override + String get loopShuffleQueue => '循环随机队列'; + @override + String get loopShuffleQueueDescription => '在队列结束时重新开始'; + @override + String get videoPlayerControls => '视频播放器控制'; + @override + String get keyboardShortcuts => '键盘快捷键'; + @override + String get keyboardShortcutsDescription => '自定义键盘快捷键'; + @override + String get debugLogging => '调试日志'; + @override + String get debugLoggingDescription => '启用详细日志记录以便故障排除'; + @override + String get viewLogs => '查看日志'; + @override + String get viewLogsDescription => '查看应用程序日志'; + @override + String get clearCache => '清除缓存'; + @override + String get clearCacheDescription => '这将清除所有缓存的图片和数据。清除缓存后,应用程序加载内容可能会变慢。'; + @override + String get clearCacheSuccess => '缓存清除成功'; + @override + String get resetSettings => '重置设置'; + @override + String get resetSettingsDescription => '这会将所有设置重置为其默认值。此操作无法撤销。'; + @override + String get resetSettingsSuccess => '设置重置成功'; + @override + String get shortcutsReset => '快捷键已重置为默认值'; + @override + String get about => '关于'; + @override + String get aboutDescription => '应用程序信息和许可证'; + @override + String get updates => '更新'; + @override + String get updateAvailable => '有可用更新'; + @override + String get checkForUpdates => '检查更新'; + @override + String get validationErrorEnterNumber => '请输入一个有效的数字'; + @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 autoSkip => '自动跳过'; + @override + String get autoSkipIntro => '自动跳过片头'; + @override + String get autoSkipIntroDescription => '几秒钟后自动跳过片头标记'; + @override + String get autoSkipCredits => '自动跳过片尾'; + @override + String get autoSkipCreditsDescription => '自动跳过片尾并播放下一集'; + @override + String get autoSkipDelay => '自动跳过延迟'; + @override + String autoSkipDelayDescription({required Object seconds}) => + '自动跳过前等待 ${seconds} 秒'; + @override + String get downloads => '下载'; + @override + String get downloadLocationDescription => '选择下载内容的存储位置'; + @override + String get downloadLocationDefault => '默认(应用存储)'; + @override + String get downloadLocationCustom => '自定义位置'; + @override + String get selectFolder => '选择文件夹'; + @override + String get resetToDefault => '重置为默认'; + @override + String currentPath({required Object path}) => '当前: ${path}'; + @override + String get downloadLocationNotSupported => 'iOS 不支持自定义下载位置'; + @override + String get downloadLocationChanged => '下载位置已更改'; + @override + String get downloadLocationReset => '下载位置已重置为默认'; + @override + String get downloadLocationInvalid => '所选文件夹不可写入'; + @override + String get downloadLocationSelectError => '选择文件夹失败'; + @override + String get downloadOnWifiOnly => '仅在 WiFi 时下载'; + @override + String get downloadOnWifiOnlyDescription => '使用蜂窝数据时禁止下载'; + @override + String get cellularDownloadBlocked => '蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。'; } // Path: search class _StringsSearchZh implements _StringsSearchEn { - _StringsSearchZh._(this._root); + _StringsSearchZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get hint => '搜索电影、系列、音乐...'; - @override String get tryDifferentTerm => '尝试不同的搜索词'; - @override String get searchYourMedia => '搜索媒体'; - @override String get enterTitleActorOrKeyword => '输入标题、演员或关键词'; + // Translations + @override + String get hint => '搜索电影、系列、音乐...'; + @override + String get tryDifferentTerm => '尝试不同的搜索词'; + @override + String get searchYourMedia => '搜索媒体'; + @override + String get enterTitleActorOrKeyword => '输入标题、演员或关键词'; } // Path: hotkeys class _StringsHotkeysZh implements _StringsHotkeysEn { - _StringsHotkeysZh._(this._root); + _StringsHotkeysZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String setShortcutFor({required Object actionName}) => '为 ${actionName} 设置快捷键'; - @override String get clearShortcut => '清除快捷键'; + // Translations + @override + String setShortcutFor({required Object actionName}) => + '为 ${actionName} 设置快捷键'; + @override + String get clearShortcut => '清除快捷键'; } // Path: pinEntry class _StringsPinEntryZh implements _StringsPinEntryEn { - _StringsPinEntryZh._(this._root); + _StringsPinEntryZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get enterPin => '输入 PIN'; - @override String get showPin => '显示 PIN'; - @override String get hidePin => '隐藏 PIN'; + // Translations + @override + String get enterPin => '输入 PIN'; + @override + String get showPin => '显示 PIN'; + @override + String get hidePin => '隐藏 PIN'; } // Path: fileInfo class _StringsFileInfoZh implements _StringsFileInfoEn { - _StringsFileInfoZh._(this._root); + _StringsFileInfoZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '文件信息'; - @override String get video => '视频'; - @override String get audio => '音频'; - @override String get file => '文件'; - @override String get advanced => '高级'; - @override String get codec => '编解码器'; - @override String get resolution => '分辨率'; - @override String get bitrate => '比特率'; - @override String get frameRate => '帧率'; - @override String get aspectRatio => '宽高比'; - @override String get profile => '配置文件'; - @override String get bitDepth => '位深度'; - @override String get colorSpace => '色彩空间'; - @override String get colorRange => '色彩范围'; - @override String get colorPrimaries => '颜色原色'; - @override String get chromaSubsampling => '色度子采样'; - @override String get channels => '声道'; - @override String get path => '路径'; - @override String get size => '大小'; - @override String get container => '容器'; - @override String get duration => '时长'; - @override String get optimizedForStreaming => '已优化用于流媒体'; - @override String get has64bitOffsets => '64位偏移量'; + // Translations + @override + String get title => '文件信息'; + @override + String get video => '视频'; + @override + String get audio => '音频'; + @override + String get file => '文件'; + @override + String get advanced => '高级'; + @override + String get codec => '编解码器'; + @override + String get resolution => '分辨率'; + @override + String get bitrate => '比特率'; + @override + String get frameRate => '帧率'; + @override + String get aspectRatio => '宽高比'; + @override + String get profile => '配置文件'; + @override + String get bitDepth => '位深度'; + @override + String get colorSpace => '色彩空间'; + @override + String get colorRange => '色彩范围'; + @override + String get colorPrimaries => '颜色原色'; + @override + String get chromaSubsampling => '色度子采样'; + @override + String get channels => '声道'; + @override + String get path => '路径'; + @override + String get size => '大小'; + @override + String get container => '容器'; + @override + String get duration => '时长'; + @override + String get optimizedForStreaming => '已优化用于流媒体'; + @override + String get has64bitOffsets => '64位偏移量'; } // Path: mediaMenu class _StringsMediaMenuZh implements _StringsMediaMenuEn { - _StringsMediaMenuZh._(this._root); + _StringsMediaMenuZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get markAsWatched => '标记为已观看'; - @override String get markAsUnwatched => '标记为未观看'; - @override String get removeFromContinueWatching => '从继续观看中移除'; - @override String get goToSeries => '转到系列'; - @override String get goToSeason => '转到季'; - @override String get shufflePlay => '随机播放'; - @override String get fileInfo => '文件信息'; + // Translations + @override + String get markAsWatched => '标记为已观看'; + @override + String get markAsUnwatched => '标记为未观看'; + @override + String get removeFromContinueWatching => '从继续观看中移除'; + @override + String get goToSeries => '转到系列'; + @override + String get goToSeason => '转到季'; + @override + String get shufflePlay => '随机播放'; + @override + String get fileInfo => '文件信息'; } // Path: accessibility class _StringsAccessibilityZh implements _StringsAccessibilityEn { - _StringsAccessibilityZh._(this._root); + _StringsAccessibilityZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String mediaCardMovie({required Object title}) => '${title}, 电影'; - @override String mediaCardShow({required Object title}) => '${title}, 电视剧'; - @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - @override String get mediaCardWatched => '已观看'; - @override String mediaCardPartiallyWatched({required Object percent}) => '已观看 ${percent} 百分比'; - @override String get mediaCardUnwatched => '未观看'; - @override String get tapToPlay => '点击播放'; + // Translations + @override + String mediaCardMovie({required Object title}) => '${title}, 电影'; + @override + String mediaCardShow({required Object title}) => '${title}, 电视剧'; + @override + String mediaCardEpisode({ + required Object title, + required Object episodeInfo, + }) => '${title}, ${episodeInfo}'; + @override + String mediaCardSeason({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + @override + String get mediaCardWatched => '已观看'; + @override + String mediaCardPartiallyWatched({required Object percent}) => + '已观看 ${percent} 百分比'; + @override + String get mediaCardUnwatched => '未观看'; + @override + String get tapToPlay => '点击播放'; } // Path: tooltips class _StringsTooltipsZh implements _StringsTooltipsEn { - _StringsTooltipsZh._(this._root); + _StringsTooltipsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get shufflePlay => '随机播放'; - @override String get markAsWatched => '标记为已观看'; - @override String get markAsUnwatched => '标记为未观看'; + // Translations + @override + String get shufflePlay => '随机播放'; + @override + String get markAsWatched => '标记为已观看'; + @override + String get markAsUnwatched => '标记为未观看'; } // Path: videoControls class _StringsVideoControlsZh implements _StringsVideoControlsEn { - _StringsVideoControlsZh._(this._root); + _StringsVideoControlsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get audioLabel => '音频'; - @override String get subtitlesLabel => '字幕'; - @override String get resetToZero => '重置为 0ms'; - @override String addTime({required Object amount, required Object unit}) => '+${amount}${unit}'; - @override String minusTime({required Object amount, required Object unit}) => '-${amount}${unit}'; - @override String playsLater({required Object label}) => '${label} 播放较晚'; - @override String playsEarlier({required Object label}) => '${label} 播放较早'; - @override String get noOffset => '无偏移'; - @override String get letterbox => '信箱模式(Letterbox)'; - @override String get fillScreen => '填充屏幕'; - @override String get stretch => '拉伸'; - @override String get lockRotation => '锁定旋转'; - @override String get unlockRotation => '解锁旋转'; - @override String get sleepTimer => '睡眠定时器'; - @override String get timerActive => '定时器已激活'; - @override String playbackWillPauseIn({required Object duration}) => '播放将在 ${duration} 后暂停'; - @override String get sleepTimerCompleted => '睡眠定时器已完成 - 播放已暂停'; - @override String get playButton => '播放'; - @override String get pauseButton => '暂停'; - @override String seekBackwardButton({required Object seconds}) => '后退 ${seconds} 秒'; - @override String seekForwardButton({required Object seconds}) => '前进 ${seconds} 秒'; - @override String get previousButton => '上一集'; - @override String get nextButton => '下一集'; - @override String get previousChapterButton => '上一章节'; - @override String get nextChapterButton => '下一章节'; - @override String get volumeButton => '音量'; - @override String get muteButton => '静音'; - @override String get unmuteButton => '取消静音'; - @override String get settingsButton => '视频设置'; - @override String get audioTrackButton => '音轨'; - @override String get subtitlesButton => '字幕'; - @override String get chaptersButton => '章节'; - @override String get versionsButton => '视频版本'; - @override String get aspectRatioButton => '宽高比'; - @override String get fullscreenButton => '进入全屏'; - @override String get exitFullscreenButton => '退出全屏'; - @override String get rotationLockButton => '旋转锁定'; - @override String get timelineSlider => '视频时间轴'; - @override String get volumeSlider => '音量调节'; - @override String get backButton => '返回'; + // Translations + @override + String get audioLabel => '音频'; + @override + String get subtitlesLabel => '字幕'; + @override + String get resetToZero => '重置为 0ms'; + @override + String addTime({required Object amount, required Object unit}) => + '+${amount}${unit}'; + @override + String minusTime({required Object amount, required Object unit}) => + '-${amount}${unit}'; + @override + String playsLater({required Object label}) => '${label} 播放较晚'; + @override + String playsEarlier({required Object label}) => '${label} 播放较早'; + @override + String get noOffset => '无偏移'; + @override + String get letterbox => '信箱模式(Letterbox)'; + @override + String get fillScreen => '填充屏幕'; + @override + String get stretch => '拉伸'; + @override + String get lockRotation => '锁定旋转'; + @override + String get unlockRotation => '解锁旋转'; + @override + String get sleepTimer => '睡眠定时器'; + @override + String get timerActive => '定时器已激活'; + @override + String playbackWillPauseIn({required Object duration}) => + '播放将在 ${duration} 后暂停'; + @override + String get sleepTimerCompleted => '睡眠定时器已完成 - 播放已暂停'; + @override + String get playButton => '播放'; + @override + String get pauseButton => '暂停'; + @override + String seekBackwardButton({required Object seconds}) => '后退 ${seconds} 秒'; + @override + String seekForwardButton({required Object seconds}) => '前进 ${seconds} 秒'; + @override + String get previousButton => '上一集'; + @override + String get nextButton => '下一集'; + @override + String get previousChapterButton => '上一章节'; + @override + String get nextChapterButton => '下一章节'; + @override + String get volumeButton => '音量'; + @override + String get muteButton => '静音'; + @override + String get unmuteButton => '取消静音'; + @override + String get settingsButton => '视频设置'; + @override + String get audioTrackButton => '音轨'; + @override + String get subtitlesButton => '字幕'; + @override + String get chaptersButton => '章节'; + @override + String get versionsButton => '视频版本'; + @override + String get aspectRatioButton => '宽高比'; + @override + String get fullscreenButton => '进入全屏'; + @override + String get exitFullscreenButton => '退出全屏'; + @override + String get rotationLockButton => '旋转锁定'; + @override + String get timelineSlider => '视频时间轴'; + @override + String get volumeSlider => '音量调节'; + @override + String get backButton => '返回'; } // Path: userStatus class _StringsUserStatusZh implements _StringsUserStatusEn { - _StringsUserStatusZh._(this._root); + _StringsUserStatusZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get admin => '管理员'; - @override String get restricted => '受限'; - @override String get protected => '受保护'; - @override String get current => '当前'; + // Translations + @override + String get admin => '管理员'; + @override + String get restricted => '受限'; + @override + String get protected => '受保护'; + @override + String get current => '当前'; } // Path: messages class _StringsMessagesZh implements _StringsMessagesEn { - _StringsMessagesZh._(this._root); + _StringsMessagesZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get markedAsWatched => '已标记为已观看'; - @override String get markedAsUnwatched => '已标记为未观看'; - @override String get removedFromContinueWatching => '已从继续观看中移除'; - @override String errorLoading({required Object error}) => '错误: ${error}'; - @override String get fileInfoNotAvailable => '文件信息不可用'; - @override String errorLoadingFileInfo({required Object error}) => '加载文件信息时出错: ${error}'; - @override String get errorLoadingSeries => '加载系列时出错'; - @override String get errorLoadingSeason => '加载季时出错'; - @override String get musicNotSupported => '尚不支持播放音乐'; - @override String get logsCleared => '日志已清除'; - @override String get logsCopied => '日志已复制到剪贴板'; - @override String get noLogsAvailable => '没有可用日志'; - @override String libraryScanning({required Object title}) => '正在扫描 “${title}”...'; - @override String libraryScanStarted({required Object title}) => '已开始扫描 “${title}” 媒体库'; - @override String libraryScanFailed({required Object error}) => '无法扫描媒体库: ${error}'; - @override String metadataRefreshing({required Object title}) => '正在刷新 “${title}” 的元数据...'; - @override String metadataRefreshStarted({required Object title}) => '已开始刷新 “${title}” 的元数据'; - @override String metadataRefreshFailed({required Object error}) => '无法刷新元数据: ${error}'; - @override String get noPlexToken => '未找到 Plex 令牌。请重新登录。'; - @override String get logoutConfirm => '你确定要登出吗?'; - @override String get noSeasonsFound => '未找到季'; - @override String get noEpisodesFound => '在第一季中未找到剧集'; - @override String get noEpisodesFoundGeneral => '未找到剧集'; - @override String get noResultsFound => '未找到结果'; - @override String sleepTimerSet({required Object label}) => '睡眠定时器已设置为 ${label}'; - @override String failedToSwitchProfile({required Object displayName}) => '无法切换到 ${displayName}'; - @override String get noItemsAvailable => '没有可用的项目'; - @override String get failedToCreatePlayQueue => '创建播放队列失败'; - @override String get failedToCreatePlayQueueNoItems => '创建播放队列失败 - 没有项目'; - @override String failedPlayback({required Object action, required Object error}) => '无法${action}: ${error}'; + // Translations + @override + String get markedAsWatched => '已标记为已观看'; + @override + String get markedAsUnwatched => '已标记为未观看'; + @override + String get markedAsWatchedOffline => '已标记为已观看 (将在联网时同步)'; + @override + String get markedAsUnwatchedOffline => '已标记为未观看 (将在联网时同步)'; + @override + String get removedFromContinueWatching => '已从继续观看中移除'; + @override + String errorLoading({required Object error}) => '错误: ${error}'; + @override + String get fileInfoNotAvailable => '文件信息不可用'; + @override + String errorLoadingFileInfo({required Object error}) => '加载文件信息时出错: ${error}'; + @override + String get errorLoadingSeries => '加载系列时出错'; + @override + String get errorLoadingSeason => '加载季时出错'; + @override + String get musicNotSupported => '尚不支持播放音乐'; + @override + String get logsCleared => '日志已清除'; + @override + String get logsCopied => '日志已复制到剪贴板'; + @override + String get noLogsAvailable => '没有可用日志'; + @override + String libraryScanning({required Object title}) => '正在扫描 “${title}”...'; + @override + String libraryScanStarted({required Object title}) => '已开始扫描 “${title}” 媒体库'; + @override + String libraryScanFailed({required Object error}) => '无法扫描媒体库: ${error}'; + @override + String metadataRefreshing({required Object title}) => + '正在刷新 “${title}” 的元数据...'; + @override + String metadataRefreshStarted({required Object title}) => + '已开始刷新 “${title}” 的元数据'; + @override + String metadataRefreshFailed({required Object error}) => '无法刷新元数据: ${error}'; + @override + String get noPlexToken => '未找到 Plex 令牌。请重新登录。'; + @override + String get logoutConfirm => '你确定要登出吗?'; + @override + String get noSeasonsFound => '未找到季'; + @override + String get noEpisodesFound => '在第一季中未找到剧集'; + @override + String get noEpisodesFoundGeneral => '未找到剧集'; + @override + String get noResultsFound => '未找到结果'; + @override + String sleepTimerSet({required Object label}) => '睡眠定时器已设置为 ${label}'; + @override + String failedToSwitchProfile({required Object displayName}) => + '无法切换到 ${displayName}'; + @override + String get noItemsAvailable => '没有可用的项目'; + @override + String get failedToCreatePlayQueue => '创建播放队列失败'; + @override + String get failedToCreatePlayQueueNoItems => '创建播放队列失败 - 没有项目'; + @override + String failedPlayback({required Object action, required Object error}) => + '无法${action}: ${error}'; + @override + String get youAreOffline => '您已离线'; + @override + String get offlineFeatureUnavailable => '此功能需要网络连接。您的下载内容和设置仍然可用。'; } // Path: profile class _StringsProfileZh implements _StringsProfileEn { - _StringsProfileZh._(this._root); + _StringsProfileZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get noUsersAvailable => '没有可用用户'; + // Translations + @override + String get noUsersAvailable => '没有可用用户'; } // Path: subtitlingStyling class _StringsSubtitlingStylingZh implements _StringsSubtitlingStylingEn { - _StringsSubtitlingStylingZh._(this._root); + _StringsSubtitlingStylingZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get stylingOptions => '样式选项'; - @override String get fontSize => '字号'; - @override String get textColor => '文本颜色'; - @override String get borderSize => '边框大小'; - @override String get borderColor => '边框颜色'; - @override String get backgroundOpacity => '背景不透明度'; - @override String get backgroundColor => '背景颜色'; + // Translations + @override + String get stylingOptions => '样式选项'; + @override + String get fontSize => '字号'; + @override + String get textColor => '文本颜色'; + @override + String get borderSize => '边框大小'; + @override + String get borderColor => '边框颜色'; + @override + String get backgroundOpacity => '背景不透明度'; + @override + String get backgroundColor => '背景颜色'; } // Path: dialog class _StringsDialogZh implements _StringsDialogEn { - _StringsDialogZh._(this._root); + _StringsDialogZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get confirmAction => '确认操作'; - @override String get areYouSure => '确定要执行此操作吗?'; - @override String get cancel => '取消'; - @override String get playNow => '立即播放'; + // Translations + @override + String get confirmAction => '确认操作'; + @override + String get areYouSure => '确定要执行此操作吗?'; + @override + String get cancel => '取消'; + @override + String get playNow => '立即播放'; } // Path: discover class _StringsDiscoverZh implements _StringsDiscoverEn { - _StringsDiscoverZh._(this._root); + _StringsDiscoverZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '发现'; - @override String get switchProfile => '切换配置文件'; - @override String get switchServer => '切换服务器'; - @override String get logout => '登出'; - @override String get noContentAvailable => '没有可用内容'; - @override String get addMediaToLibraries => '请向你的媒体库添加一些媒体'; - @override String get continueWatching => '继续观看'; - @override String get recentlyAdded => '最近添加'; - @override String get play => '播放'; - @override String get resume => '继续'; - @override String playEpisode({required Object season, required Object episode}) => '播放 S${season}, E${episode}'; - @override String resumeEpisode({required Object season, required Object episode}) => '继续 S${season}, E${episode}'; - @override String get pause => '暂停'; - @override String get overview => '概述'; - @override String get cast => '演员表'; - @override String get seasons => '季数'; - @override String get studio => '制作公司'; - @override String get rating => '年龄分级'; - @override String get watched => '已观看'; - @override String episodeCount({required Object count}) => '${count} 集'; - @override String watchedProgress({required Object watched, required Object total}) => '已观看 ${watched}/${total} 集'; - @override String get movie => '电影'; - @override String get tvShow => '电视剧'; - @override String minutesLeft({required Object minutes}) => '剩余 ${minutes} 分钟'; + // Translations + @override + String get title => '发现'; + @override + String get switchProfile => '切换配置文件'; + @override + String get switchServer => '切换服务器'; + @override + String get logout => '登出'; + @override + String get noContentAvailable => '没有可用内容'; + @override + String get addMediaToLibraries => '请向你的媒体库添加一些媒体'; + @override + String get continueWatching => '继续观看'; + @override + String get recentlyAdded => '最近添加'; + @override + String get play => '播放'; + @override + String get resume => '继续'; + @override + String playEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String resumeEpisode({required Object season, required Object episode}) => + 'S${season}E${episode}'; + @override + String get pause => '暂停'; + @override + String get overview => '概述'; + @override + String get cast => '演员表'; + @override + String get seasons => '季数'; + @override + String get studio => '制作公司'; + @override + String get rating => '年龄分级'; + @override + String get watched => '已观看'; + @override + String episodeCount({required Object count}) => '${count} 集'; + @override + String watchedProgress({required Object watched, required Object total}) => + '已观看 ${watched}/${total} 集'; + @override + String get movie => '电影'; + @override + String get tvShow => '电视剧'; + @override + String minutesLeft({required Object minutes}) => '剩余 ${minutes} 分钟'; } // Path: errors class _StringsErrorsZh implements _StringsErrorsEn { - _StringsErrorsZh._(this._root); + _StringsErrorsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String searchFailed({required Object error}) => '搜索失败: ${error}'; - @override String connectionTimeout({required Object context}) => '加载 ${context} 时连接超时'; - @override String get connectionFailed => '无法连接到 Plex 服务器'; - @override String failedToLoad({required Object context, required Object error}) => '无法加载 ${context}: ${error}'; - @override String get noClientAvailable => '没有可用客户端'; - @override String authenticationFailed({required Object error}) => '验证失败: ${error}'; - @override String get couldNotLaunchUrl => '无法打开授权 URL'; - @override String get pleaseEnterToken => '请输入一个令牌'; - @override String get invalidToken => '令牌无效'; - @override String failedToVerifyToken({required Object error}) => '无法验证令牌: ${error}'; - @override String failedToSwitchProfile({required Object displayName}) => '无法切换到 ${displayName}'; - @override String get connectionFailedGeneric => '连接失败'; + // Translations + @override + String searchFailed({required Object error}) => '搜索失败: ${error}'; + @override + String connectionTimeout({required Object context}) => '加载 ${context} 时连接超时'; + @override + String get connectionFailed => '无法连接到 Plex 服务器'; + @override + String failedToLoad({required Object context, required Object error}) => + '无法加载 ${context}: ${error}'; + @override + String get noClientAvailable => '没有可用客户端'; + @override + String authenticationFailed({required Object error}) => '验证失败: ${error}'; + @override + String get couldNotLaunchUrl => '无法打开授权 URL'; + @override + String get pleaseEnterToken => '请输入一个令牌'; + @override + String get invalidToken => '令牌无效'; + @override + String failedToVerifyToken({required Object error}) => '无法验证令牌: ${error}'; + @override + String failedToSwitchProfile({required Object displayName}) => + '无法切换到 ${displayName}'; + @override + String get connectionFailedGeneric => '连接失败'; } // Path: libraries class _StringsLibrariesZh implements _StringsLibrariesEn { - _StringsLibrariesZh._(this._root); + _StringsLibrariesZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '媒体库'; - @override String get scanLibraryFiles => '扫描媒体库文件'; - @override String get scanLibrary => '扫描媒体库'; - @override String get analyze => '分析'; - @override String get analyzeLibrary => '分析媒体库'; - @override String get refreshMetadata => '刷新元数据'; - @override String get emptyTrash => '清空回收站'; - @override String emptyingTrash({required Object title}) => '正在清空 “${title}” 的回收站...'; - @override String trashEmptied({required Object title}) => '已清空 “${title}” 的回收站'; - @override String failedToEmptyTrash({required Object error}) => '无法清空回收站: ${error}'; - @override String analyzing({required Object title}) => '正在分析 “${title}”...'; - @override String analysisStarted({required Object title}) => '已开始分析 “${title}”'; - @override String failedToAnalyze({required Object error}) => '无法分析媒体库: ${error}'; - @override String get noLibrariesFound => '未找到媒体库'; - @override String get thisLibraryIsEmpty => '此媒体库为空'; - @override String get all => '全部'; - @override String get clearAll => '全部清除'; - @override String scanLibraryConfirm({required Object title}) => '确定要扫描 “${title}” 吗?'; - @override String analyzeLibraryConfirm({required Object title}) => '确定要分析 “${title}” 吗?'; - @override String refreshMetadataConfirm({required Object title}) => '确定要刷新 “${title}” 的元数据吗?'; - @override String emptyTrashConfirm({required Object title}) => '确定要清空 “${title}” 的回收站吗?'; - @override String get manageLibraries => '管理媒体库'; - @override String get sort => '排序'; - @override String get sortBy => '排序依据'; - @override String get filters => '筛选器'; - @override String loadingLibraryWithCount({required Object count}) => '正在加载媒体库... (已加载 ${count} 个项目)'; - @override String get confirmActionMessage => '确定要执行此操作吗?'; - @override String get showLibrary => '显示媒体库'; - @override String get hideLibrary => '隐藏媒体库'; - @override String get libraryOptions => '媒体库选项'; - @override String get content => '媒体库内容'; - @override String get selectLibrary => '选择媒体库'; - @override String filtersWithCount({required Object count}) => '筛选器(${count})'; - @override String get noRecommendations => '暂无推荐'; - @override String get noCollections => '此媒体库中没有合集'; - @override String get noFoldersFound => '未找到文件夹'; - @override String get folders => '文件夹'; - @override late final _StringsLibrariesTabsZh tabs = _StringsLibrariesTabsZh._(_root); - @override late final _StringsLibrariesGroupingsZh groupings = _StringsLibrariesGroupingsZh._(_root); + // Translations + @override + String get title => '媒体库'; + @override + String get scanLibraryFiles => '扫描媒体库文件'; + @override + String get scanLibrary => '扫描媒体库'; + @override + String get analyze => '分析'; + @override + String get analyzeLibrary => '分析媒体库'; + @override + String get refreshMetadata => '刷新元数据'; + @override + String get emptyTrash => '清空回收站'; + @override + String emptyingTrash({required Object title}) => '正在清空 “${title}” 的回收站...'; + @override + String trashEmptied({required Object title}) => '已清空 “${title}” 的回收站'; + @override + String failedToEmptyTrash({required Object error}) => '无法清空回收站: ${error}'; + @override + String analyzing({required Object title}) => '正在分析 “${title}”...'; + @override + String analysisStarted({required Object title}) => '已开始分析 “${title}”'; + @override + String failedToAnalyze({required Object error}) => '无法分析媒体库: ${error}'; + @override + String get noLibrariesFound => '未找到媒体库'; + @override + String get thisLibraryIsEmpty => '此媒体库为空'; + @override + String get all => '全部'; + @override + String get clearAll => '全部清除'; + @override + String scanLibraryConfirm({required Object title}) => '确定要扫描 “${title}” 吗?'; + @override + String analyzeLibraryConfirm({required Object title}) => + '确定要分析 “${title}” 吗?'; + @override + String refreshMetadataConfirm({required Object title}) => + '确定要刷新 “${title}” 的元数据吗?'; + @override + String emptyTrashConfirm({required Object title}) => + '确定要清空 “${title}” 的回收站吗?'; + @override + String get manageLibraries => '管理媒体库'; + @override + String get sort => '排序'; + @override + String get sortBy => '排序依据'; + @override + String get filters => '筛选器'; + @override + String loadingLibraryWithCount({required Object count}) => + '正在加载媒体库... (已加载 ${count} 个项目)'; + @override + String get confirmActionMessage => '确定要执行此操作吗?'; + @override + String get showLibrary => '显示媒体库'; + @override + String get hideLibrary => '隐藏媒体库'; + @override + String get libraryOptions => '媒体库选项'; + @override + String get content => '媒体库内容'; + @override + String get selectLibrary => '选择媒体库'; + @override + String filtersWithCount({required Object count}) => '筛选器(${count})'; + @override + String get noRecommendations => '暂无推荐'; + @override + String get noCollections => '此媒体库中没有合集'; + @override + String get noFoldersFound => '未找到文件夹'; + @override + String get folders => '文件夹'; + @override + late final _StringsLibrariesTabsZh tabs = _StringsLibrariesTabsZh._(_root); + @override + late final _StringsLibrariesGroupingsZh groupings = + _StringsLibrariesGroupingsZh._(_root); } // Path: about class _StringsAboutZh implements _StringsAboutEn { - _StringsAboutZh._(this._root); + _StringsAboutZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '关于'; - @override String get openSourceLicenses => '开源许可证'; - @override String versionLabel({required Object version}) => '版本 ${version}'; - @override String get appDescription => '一款精美的 Flutter Plex 客户端'; - @override String get viewLicensesDescription => '查看第三方库的许可证'; + // Translations + @override + String get title => '关于'; + @override + String get openSourceLicenses => '开源许可证'; + @override + String versionLabel({required Object version}) => '版本 ${version}'; + @override + String get appDescription => '一款精美的 Flutter Plex 客户端'; + @override + String get viewLicensesDescription => '查看第三方库的许可证'; } // Path: serverSelection class _StringsServerSelectionZh implements _StringsServerSelectionEn { - _StringsServerSelectionZh._(this._root); + _StringsServerSelectionZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get connectingToServer => '正在连接服务器...'; - @override String get connectingToAllServers => '正在连接所有服务器...'; - @override String get connectToAllServers => '连接所有服务器'; - @override String get allServerConnectionsFailed => '无法连接到任何服务器。请检查你的网络并重试。'; - @override String foundServers({required Object count}) => '找到 ${count} 个服务器'; - @override String get orSelectIndividually => '或单独选择一个服务器:'; - @override String get serverDebugCopied => '服务器调试数据已复制到剪贴板'; - @override String get copyDebugData => '复制调试数据'; - @override String get noServersFound => '未找到服务器'; - @override String noServersFoundForAccount({required Object username, required Object email}) => '未找到 ${username} (${email}) 的服务器'; - @override String malformedServerData({required Object count}) => '找到 ${count} 个数据格式错误的服务器。没有可用的有效服务器。'; - @override String get incompleteServerInfo => '某些服务器信息不完整,已被跳过。请检查你的 Plex.tv 账户。'; - @override String get incompleteConnectionInfo => '服务器连接信息不完整。请重试。'; - @override String malformedServerInfo({required Object message}) => '服务器信息格式错误: ${message}'; - @override String get networkConnectionFailed => '网络连接失败。请检查你的网络连接并重试。'; - @override String get authenticationFailed => '验证失败。请重新登录。'; - @override String get plexServiceUnavailable => 'Plex 服务不可用。请稍后重试。'; - @override String failedToLoadServers({required Object error}) => '无法加载服务器: ${error}'; + // Translations + @override + String get connectingToServer => '正在连接服务器...'; + @override + String get connectingToAllServers => '正在连接所有服务器...'; + @override + String get connectToAllServers => '连接所有服务器'; + @override + String get allServerConnectionsFailed => '无法连接到任何服务器。请检查你的网络并重试。'; + @override + String foundServers({required Object count}) => '找到 ${count} 个服务器'; + @override + String get orSelectIndividually => '或单独选择一个服务器:'; + @override + String get serverDebugCopied => '服务器调试数据已复制到剪贴板'; + @override + String get copyDebugData => '复制调试数据'; + @override + String get noServersFound => '未找到服务器'; + @override + String noServersFoundForAccount({ + required Object username, + required Object email, + }) => '未找到 ${username} (${email}) 的服务器'; + @override + String malformedServerData({required Object count}) => + '找到 ${count} 个数据格式错误的服务器。没有可用的有效服务器。'; + @override + String get incompleteServerInfo => '某些服务器信息不完整,已被跳过。请检查你的 Plex.tv 账户。'; + @override + String get incompleteConnectionInfo => '服务器连接信息不完整。请重试。'; + @override + String malformedServerInfo({required Object message}) => + '服务器信息格式错误: ${message}'; + @override + String get networkConnectionFailed => '网络连接失败。请检查你的网络连接并重试。'; + @override + String get authenticationFailed => '验证失败。请重新登录。'; + @override + String get plexServiceUnavailable => 'Plex 服务不可用。请稍后重试。'; + @override + String failedToLoadServers({required Object error}) => '无法加载服务器: ${error}'; } // Path: hubDetail class _StringsHubDetailZh implements _StringsHubDetailEn { - _StringsHubDetailZh._(this._root); + _StringsHubDetailZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '标题'; - @override String get releaseYear => '发行年份'; - @override String get dateAdded => '添加日期'; - @override String get rating => '评分'; - @override String get noItemsFound => '未找到项目'; + // Translations + @override + String get title => '标题'; + @override + String get releaseYear => '发行年份'; + @override + String get dateAdded => '添加日期'; + @override + String get rating => '评分'; + @override + String get noItemsFound => '未找到项目'; } // Path: logs class _StringsLogsZh implements _StringsLogsEn { - _StringsLogsZh._(this._root); + _StringsLogsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '日志'; - @override String get clearLogs => '清除日志'; - @override String get copyLogs => '复制日志'; - @override String get exportLogs => '导出日志'; - @override String get noLogsToShow => '没有可显示的日志'; - @override String get error => '错误:'; - @override String get stackTrace => '堆栈跟踪 (Stack Trace):'; + // Translations + @override + String get title => '日志'; + @override + String get clearLogs => '清除日志'; + @override + String get copyLogs => '复制日志'; + @override + String get exportLogs => '导出日志'; + @override + String get noLogsToShow => '没有可显示的日志'; + @override + String get error => '错误:'; + @override + String get stackTrace => '堆栈跟踪 (Stack Trace):'; } // Path: licenses class _StringsLicensesZh implements _StringsLicensesEn { - _StringsLicensesZh._(this._root); + _StringsLicensesZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get relatedPackages => '相关软件包'; - @override String get license => '许可证'; - @override String licenseNumber({required Object number}) => '许可证 ${number}'; - @override String licensesCount({required Object count}) => '${count} 个许可证'; + // Translations + @override + String get relatedPackages => '相关软件包'; + @override + String get license => '许可证'; + @override + String licenseNumber({required Object number}) => '许可证 ${number}'; + @override + String licensesCount({required Object count}) => '${count} 个许可证'; } // Path: navigation class _StringsNavigationZh implements _StringsNavigationEn { - _StringsNavigationZh._(this._root); + _StringsNavigationZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get home => '主页'; - @override String get search => '搜索'; - @override String get libraries => '媒体库'; - @override String get settings => '设置'; + // Translations + @override + String get home => '主页'; + @override + String get search => '搜索'; + @override + String get libraries => '媒体库'; + @override + String get settings => '设置'; + @override + String get downloads => '下载'; +} + +// Path: downloads +class _StringsDownloadsZh implements _StringsDownloadsEn { + _StringsDownloadsZh._(this._root); + + @override + final _StringsZh _root; // ignore: unused_field + + // Translations + @override + String get title => '下载'; + @override + String get manage => '管理'; + @override + String get tvShows => '电视剧'; + @override + String get movies => '电影'; + @override + String get queue => '队列'; + @override + String get noDownloads => '暂无下载'; + @override + String get noDownloadsDescription => '下载的内容将在此处显示以供离线观看'; + @override + String get browseContent => '浏览内容'; + @override + String get downloading => '下载中'; + @override + String get downloaded => '已下载'; + @override + String get paused => '已暂停'; + @override + String get failed => '失败'; + @override + String get queued => '排队中'; + @override + String get downloadNow => '下载'; + @override + String get deleteDownload => '删除下载'; + @override + String get cancelDownload => '取消下载'; + @override + String get pauseDownload => '暂停下载'; + @override + String get resumeDownload => '恢复下载'; + @override + String get retryDownload => '重试下载'; + @override + String get downloadComplete => '下载完成'; + @override + String get downloadFailed => '下载失败'; + @override + String get downloadCancelled => '下载已取消'; + @override + String get downloadQueued => '下载已排队'; + @override + String episodesQueued({required Object count}) => '${count} 集已加入下载队列'; + @override + String get downloadDeleted => '下载已删除'; + @override + String get deleteConfirmTitle => '删除下载?'; + @override + String deleteConfirm({required Object title}) => + '确定要删除 "${title}" 吗?下载的文件将从您的设备中删除。'; + @override + String get deleteConfirmMessage => '下载的文件将从您的设备中删除。'; + @override + String deletingWithProgress({ + required Object title, + required Object current, + required Object total, + }) => '正在删除 ${title}... (${current}/${total})'; + @override + String deletingSimple({required Object title}) => '正在删除 ${title}...'; + @override + String get storageUsed => '已使用存储空间'; + @override + String get availableOffline => '可离线使用'; + @override + String get downloadingSeason => '正在下载季'; + @override + String get downloadingEpisode => '正在下载剧集'; +} + +// Path: offline +class _StringsOfflineZh implements _StringsOfflineEn { + _StringsOfflineZh._(this._root); + + @override + final _StringsZh _root; // ignore: unused_field + + // Translations + @override + String get youAreOffline => '您已离线'; + @override + String get offlineMode => '离线模式'; + @override + String get backOnline => '已恢复在线'; + @override + String get showingDownloadedContent => '仅显示已下载的内容'; + @override + String get noInternetConnection => '无网络连接'; + @override + String get serversUnreachable => '无法连接到 Plex 服务器'; + @override + String get connectToDownload => '连接网络以下载内容'; } // Path: playlists class _StringsPlaylistsZh implements _StringsPlaylistsEn { - _StringsPlaylistsZh._(this._root); + _StringsPlaylistsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '播放列表'; - @override String get noPlaylists => '未找到播放列表'; - @override String get create => '创建播放列表'; - @override String get newPlaylist => '新播放列表'; - @override String get playlistName => '播放列表名称'; - @override String get enterPlaylistName => '输入播放列表名称'; - @override String get edit => '编辑播放列表'; - @override String get delete => '删除播放列表'; - @override String get addTo => '添加到播放列表'; - @override String get addItems => '添加项目'; - @override String get removeItem => '从播放列表中移除'; - @override String get clearPlaylist => '清空播放列表'; - @override String get playAll => '全部播放'; - @override String get shuffle => '随机播放'; - @override String get smartPlaylist => '智能播放列表'; - @override String get regularPlaylist => '普通播放列表'; - @override String itemCount({required Object count}) => '${count} 个项目'; - @override String get oneItem => '1 个项目'; - @override String get emptyPlaylist => '此播放列表为空'; - @override String get deleteConfirm => '删除播放列表?'; - @override String deleteMessage({required Object name}) => '确定要删除 "${name}" 吗?'; - @override String get created => '播放列表已创建'; - @override String get updated => '播放列表已更新'; - @override String get deleted => '播放列表已删除'; - @override String get itemAdded => '已添加到播放列表'; - @override String get itemRemoved => '已从播放列表中移除'; - @override String get selectPlaylist => '选择播放列表'; - @override String get createNewPlaylist => '创建新播放列表'; - @override String get errorCreating => '创建播放列表失败'; - @override String get errorDeleting => '删除播放列表失败'; - @override String get errorLoading => '加载播放列表失败'; - @override String get errorAdding => '添加到播放列表失败'; - @override String get errorReordering => '重新排序播放列表项目失败'; - @override String get errorRemoving => '从播放列表中移除失败'; - @override String get playlist => '播放列表'; + // Translations + @override + String get title => '播放列表'; + @override + String get noPlaylists => '未找到播放列表'; + @override + String get create => '创建播放列表'; + @override + String get newPlaylist => '新播放列表'; + @override + String get playlistName => '播放列表名称'; + @override + String get enterPlaylistName => '输入播放列表名称'; + @override + String get edit => '编辑播放列表'; + @override + String get delete => '删除播放列表'; + @override + String get addTo => '添加到播放列表'; + @override + String get addItems => '添加项目'; + @override + String get removeItem => '从播放列表中移除'; + @override + String get clearPlaylist => '清空播放列表'; + @override + String get playAll => '全部播放'; + @override + String get shuffle => '随机播放'; + @override + String get smartPlaylist => '智能播放列表'; + @override + String get regularPlaylist => '普通播放列表'; + @override + String itemCount({required Object count}) => '${count} 个项目'; + @override + String get oneItem => '1 个项目'; + @override + String get emptyPlaylist => '此播放列表为空'; + @override + String get deleteConfirm => '删除播放列表?'; + @override + String deleteMessage({required Object name}) => '确定要删除 "${name}" 吗?'; + @override + String get created => '播放列表已创建'; + @override + String get updated => '播放列表已更新'; + @override + String get deleted => '播放列表已删除'; + @override + String get itemAdded => '已添加到播放列表'; + @override + String get itemRemoved => '已从播放列表中移除'; + @override + String get selectPlaylist => '选择播放列表'; + @override + String get createNewPlaylist => '创建新播放列表'; + @override + String get errorCreating => '创建播放列表失败'; + @override + String get errorDeleting => '删除播放列表失败'; + @override + String get errorLoading => '加载播放列表失败'; + @override + String get errorAdding => '添加到播放列表失败'; + @override + String get errorReordering => '重新排序播放列表项目失败'; + @override + String get errorRemoving => '从播放列表中移除失败'; + @override + String get playlist => '播放列表'; } // Path: collections class _StringsCollectionsZh implements _StringsCollectionsEn { - _StringsCollectionsZh._(this._root); + _StringsCollectionsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get title => '合集'; - @override String get collection => '合集'; - @override String get empty => '合集为空'; - @override String get noItems => '此合集没有项目'; - @override String get unknownLibrarySection => '无法删除:未知的媒体库分区'; - @override String get deleteCollection => '删除合集'; - @override String deleteConfirm({required Object title}) => '确定要删除"${title}"吗?此操作无法撤销。'; - @override String get deleted => '已删除合集'; - @override String get deleteFailed => '删除合集失败'; - @override String deleteFailedWithError({required Object error}) => '删除合集失败:${error}'; - @override String failedToLoadItems({required Object error}) => '加载合集项目失败:${error}'; - @override String get addTo => '添加到合集'; - @override String get selectCollection => '选择合集'; - @override String get createNewCollection => '创建新合集'; - @override String get collectionName => '合集名称'; - @override String get enterCollectionName => '输入合集名称'; - @override String get addedToCollection => '已添加到合集'; - @override String get errorAddingToCollection => '添加到合集失败'; - @override String get created => '已创建合集'; - @override String get removeFromCollection => '从合集移除'; - @override String removeFromCollectionConfirm({required Object title}) => '将“${title}”从此合集移除?'; - @override String get removedFromCollection => '已从合集移除'; - @override String get removeFromCollectionFailed => '从合集移除失败'; - @override String removeFromCollectionError({required Object error}) => '从合集移除时出错:${error}'; + // Translations + @override + String get title => '合集'; + @override + String get collection => '合集'; + @override + String get empty => '合集为空'; + @override + String get noItems => '此合集没有项目'; + @override + String get unknownLibrarySection => '无法删除:未知的媒体库分区'; + @override + String get deleteCollection => '删除合集'; + @override + String deleteConfirm({required Object title}) => '确定要删除"${title}"吗?此操作无法撤销。'; + @override + String get deleted => '已删除合集'; + @override + String get deleteFailed => '删除合集失败'; + @override + String deleteFailedWithError({required Object error}) => '删除合集失败:${error}'; + @override + String failedToLoadItems({required Object error}) => '加载合集项目失败:${error}'; + @override + String get addTo => '添加到合集'; + @override + String get selectCollection => '选择合集'; + @override + String get createNewCollection => '创建新合集'; + @override + String get collectionName => '合集名称'; + @override + String get enterCollectionName => '输入合集名称'; + @override + String get addedToCollection => '已添加到合集'; + @override + String get errorAddingToCollection => '添加到合集失败'; + @override + String get created => '已创建合集'; + @override + String get removeFromCollection => '从合集移除'; + @override + String removeFromCollectionConfirm({required Object title}) => + '将“${title}”从此合集移除?'; + @override + String get removedFromCollection => '已从合集移除'; + @override + String get removeFromCollectionFailed => '从合集移除失败'; + @override + String removeFromCollectionError({required Object error}) => + '从合集移除时出错:${error}'; } // Path: libraries.tabs class _StringsLibrariesTabsZh implements _StringsLibrariesTabsEn { - _StringsLibrariesTabsZh._(this._root); + _StringsLibrariesTabsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get recommended => '推荐'; - @override String get browse => '浏览'; - @override String get collections => '合集'; - @override String get playlists => '播放列表'; + // Translations + @override + String get recommended => '推荐'; + @override + String get browse => '浏览'; + @override + String get collections => '合集'; + @override + String get playlists => '播放列表'; } // Path: libraries.groupings class _StringsLibrariesGroupingsZh implements _StringsLibrariesGroupingsEn { - _StringsLibrariesGroupingsZh._(this._root); + _StringsLibrariesGroupingsZh._(this._root); - @override final _StringsZh _root; // ignore: unused_field + @override + final _StringsZh _root; // ignore: unused_field - // Translations - @override String get all => '全部'; - @override String get movies => '电影'; - @override String get shows => '剧集'; - @override String get seasons => '季'; - @override String get episodes => '集'; - @override String get folders => '文件夹'; + // Translations + @override + String get all => '全部'; + @override + String get movies => '电影'; + @override + String get shows => '剧集'; + @override + String get seasons => '季'; + @override + String get episodes => '集'; + @override + String get folders => '文件夹'; } /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. extension on Translations { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return 'Loading...'; - case 'auth.signInWithPlex': return 'Sign in with Plex'; - case 'auth.showQRCode': return 'Show QR Code'; - case 'auth.cancel': return 'Cancel'; - case 'auth.authenticate': return 'Authenticate'; - case 'auth.retry': return 'Retry'; - case 'auth.debugEnterToken': return 'Debug: Enter Plex Token'; - case 'auth.plexTokenLabel': return 'Plex Auth Token'; - case 'auth.plexTokenHint': return 'Enter your Plex.tv token'; - case 'auth.authenticationTimeout': return 'Authentication timed out. Please try again.'; - case 'auth.scanQRCodeInstruction': return 'Scan this QR code with a device logged into Plex to authenticate.'; - case 'auth.waitingForAuth': return 'Waiting for authentication...\nPlease complete sign-in in your browser.'; - case 'common.cancel': return 'Cancel'; - case 'common.save': return 'Save'; - case 'common.close': return 'Close'; - case 'common.clear': return 'Clear'; - case 'common.reset': return 'Reset'; - case 'common.later': return 'Later'; - case 'common.submit': return 'Submit'; - case 'common.confirm': return 'Confirm'; - case 'common.retry': return 'Retry'; - case 'common.playNow': return 'Play Now'; - case 'common.logout': return 'Logout'; - case 'common.online': return 'Online'; - case 'common.offline': return 'Offline'; - case 'common.owned': return 'Owned'; - case 'common.shared': return 'Shared'; - case 'common.current': return 'CURRENT'; - case 'common.unknown': return 'Unknown'; - case 'common.refresh': return 'Refresh'; - case 'common.yes': return 'Yes'; - case 'common.no': return 'No'; - case 'common.server': return 'Server'; - case 'common.delete': return 'Delete'; - case 'common.shuffle': return 'Shuffle'; - case 'common.addTo': return 'Add to...'; - case 'screens.licenses': return 'Licenses'; - case 'screens.switchProfile': return 'Switch Profile'; - case 'screens.subtitleStyling': return 'Subtitle Styling'; - case 'screens.search': return 'Search'; - case 'screens.logs': return 'Logs'; - case 'update.available': return 'Update Available'; - case 'update.versionAvailable': return ({required Object version}) => 'Version ${version} is available'; - case 'update.currentVersion': return ({required Object version}) => 'Current: ${version}'; - case 'update.skipVersion': return 'Skip This Version'; - case 'update.viewRelease': return 'View Release'; - case 'update.latestVersion': return 'You are on the latest version'; - case 'update.checkFailed': return 'Failed to check for updates'; - case 'settings.title': return 'Settings'; - case 'settings.language': return 'Language'; - case 'settings.theme': return 'Theme'; - case 'settings.appearance': return 'Appearance'; - case 'settings.videoPlayback': return 'Video Playback'; - case 'settings.shufflePlay': return 'Shuffle Play'; - case 'settings.advanced': return 'Advanced'; - case 'settings.useSeasonPostersDescription': return 'Show season poster instead of series poster for episodes'; - case 'settings.showHeroSectionDescription': return 'Display featured content carousel on home screen'; - case 'settings.secondsLabel': return 'Seconds'; - case 'settings.minutesLabel': return 'Minutes'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => 'Enter duration (${min}-${max})'; - case 'settings.systemTheme': return 'System'; - case 'settings.systemThemeDescription': return 'Follow system settings'; - case 'settings.lightTheme': return 'Light'; - case 'settings.darkTheme': return 'Dark'; - case 'settings.libraryDensity': return 'Library Density'; - case 'settings.compact': return 'Compact'; - case 'settings.compactDescription': return 'Smaller cards, more items visible'; - case 'settings.normal': return 'Normal'; - case 'settings.normalDescription': return 'Default size'; - case 'settings.comfortable': return 'Comfortable'; - case 'settings.comfortableDescription': return 'Larger cards, fewer items visible'; - case 'settings.viewMode': return 'View Mode'; - case 'settings.gridView': return 'Grid'; - case 'settings.gridViewDescription': return 'Display items in a grid layout'; - case 'settings.listView': return 'List'; - case 'settings.listViewDescription': return 'Display items in a list layout'; - case 'settings.useSeasonPosters': return 'Use Season Posters'; - case 'settings.showHeroSection': return 'Show Hero Section'; - case 'settings.hardwareDecoding': return 'Hardware Decoding'; - case 'settings.hardwareDecodingDescription': return 'Use hardware acceleration when available'; - case 'settings.bufferSize': return 'Buffer Size'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return 'Subtitle Styling'; - case 'settings.subtitleStylingDescription': return 'Customize subtitle appearance'; - case 'settings.smallSkipDuration': return 'Small Skip Duration'; - case 'settings.largeSkipDuration': return 'Large Skip Duration'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} seconds'; - case 'settings.defaultSleepTimer': return 'Default Sleep Timer'; - case 'settings.minutesUnit': return ({required Object minutes}) => '${minutes} minutes'; - case 'settings.rememberTrackSelections': return 'Remember track selections per show/movie'; - case 'settings.rememberTrackSelectionsDescription': return 'Automatically save audio and subtitle language preferences when you change tracks during playback'; - case 'settings.unwatchedOnly': return 'Unwatched Only'; - case 'settings.unwatchedOnlyDescription': return 'Only include unwatched episodes in shuffle queue'; - case 'settings.shuffleOrderNavigation': return 'Shuffle Order Navigation'; - case 'settings.shuffleOrderNavigationDescription': return 'Next/previous buttons follow shuffled order'; - case 'settings.loopShuffleQueue': return 'Loop Shuffle Queue'; - case 'settings.loopShuffleQueueDescription': return 'Restart queue when reaching the end'; - case 'settings.videoPlayerControls': return 'Video Player Controls'; - case 'settings.keyboardShortcuts': return 'Keyboard Shortcuts'; - case 'settings.keyboardShortcutsDescription': return 'Customize keyboard shortcuts'; - case 'settings.debugLogging': return 'Debug Logging'; - case 'settings.debugLoggingDescription': return 'Enable detailed logging for troubleshooting'; - case 'settings.viewLogs': return 'View Logs'; - case 'settings.viewLogsDescription': return 'View application logs'; - case 'settings.clearCache': return 'Clear Cache'; - case 'settings.clearCacheDescription': return 'This will clear all cached images and data. The app may take longer to load content after clearing the cache.'; - case 'settings.clearCacheSuccess': return 'Cache cleared successfully'; - case 'settings.resetSettings': return 'Reset Settings'; - case 'settings.resetSettingsDescription': return 'This will reset all settings to their default values. This action cannot be undone.'; - case 'settings.resetSettingsSuccess': return 'Settings reset successfully'; - case 'settings.shortcutsReset': return 'Shortcuts reset to defaults'; - case 'settings.about': return 'About'; - case 'settings.aboutDescription': return 'App information and licenses'; - case 'settings.updates': return 'Updates'; - case 'settings.updateAvailable': return 'Update Available'; - case 'settings.checkForUpdates': return 'Check for Updates'; - case 'settings.validationErrorEnterNumber': return 'Please enter a valid number'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => 'Duration must be between ${min} and ${max} ${unit}'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => 'Shortcut already assigned to ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => 'Shortcut updated for ${action}'; - case 'settings.autoSkip': return 'Auto Skip'; - case 'settings.autoSkipIntro': return 'Auto Skip Intro'; - case 'settings.autoSkipIntroDescription': return 'Automatically skip intro markers after a few seconds'; - case 'settings.autoSkipCredits': return 'Auto Skip Credits'; - case 'settings.autoSkipCreditsDescription': return 'Automatically skip credits and play next episode'; - case 'settings.autoSkipDelay': return 'Auto Skip Delay'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => 'Wait ${seconds} seconds before auto-skipping'; - case 'search.hint': return 'Search movies, shows, music...'; - case 'search.tryDifferentTerm': return 'Try a different search term'; - case 'search.searchYourMedia': return 'Search your media'; - case 'search.enterTitleActorOrKeyword': return 'Enter a title, actor, or keyword'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => 'Set Shortcut for ${actionName}'; - case 'hotkeys.clearShortcut': return 'Clear shortcut'; - case 'pinEntry.enterPin': return 'Enter PIN'; - case 'pinEntry.showPin': return 'Show PIN'; - case 'pinEntry.hidePin': return 'Hide PIN'; - case 'fileInfo.title': return 'File Info'; - case 'fileInfo.video': return 'Video'; - case 'fileInfo.audio': return 'Audio'; - case 'fileInfo.file': return 'File'; - case 'fileInfo.advanced': return 'Advanced'; - case 'fileInfo.codec': return 'Codec'; - case 'fileInfo.resolution': return 'Resolution'; - case 'fileInfo.bitrate': return 'Bitrate'; - case 'fileInfo.frameRate': return 'Frame Rate'; - case 'fileInfo.aspectRatio': return 'Aspect Ratio'; - case 'fileInfo.profile': return 'Profile'; - case 'fileInfo.bitDepth': return 'Bit Depth'; - case 'fileInfo.colorSpace': return 'Color Space'; - case 'fileInfo.colorRange': return 'Color Range'; - case 'fileInfo.colorPrimaries': return 'Color Primaries'; - case 'fileInfo.chromaSubsampling': return 'Chroma Subsampling'; - case 'fileInfo.channels': return 'Channels'; - case 'fileInfo.path': return 'Path'; - case 'fileInfo.size': return 'Size'; - case 'fileInfo.container': return 'Container'; - case 'fileInfo.duration': return 'Duration'; - case 'fileInfo.optimizedForStreaming': return 'Optimized for Streaming'; - case 'fileInfo.has64bitOffsets': return '64-bit Offsets'; - case 'mediaMenu.markAsWatched': return 'Mark as Watched'; - case 'mediaMenu.markAsUnwatched': return 'Mark as Unwatched'; - case 'mediaMenu.removeFromContinueWatching': return 'Remove from Continue Watching'; - case 'mediaMenu.goToSeries': return 'Go to series'; - case 'mediaMenu.goToSeason': return 'Go to season'; - case 'mediaMenu.shufflePlay': return 'Shuffle Play'; - case 'mediaMenu.fileInfo': return 'File Info'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return 'watched'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; - case 'accessibility.mediaCardUnwatched': return 'unwatched'; - case 'accessibility.tapToPlay': return 'Tap to play'; - case 'tooltips.shufflePlay': return 'Shuffle play'; - case 'tooltips.markAsWatched': return 'Mark as watched'; - case 'tooltips.markAsUnwatched': return 'Mark as unwatched'; - case 'videoControls.audioLabel': return 'Audio'; - case 'videoControls.subtitlesLabel': return 'Subtitles'; - case 'videoControls.resetToZero': return 'Reset to 0ms'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} plays later'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} plays earlier'; - case 'videoControls.noOffset': return 'No offset'; - case 'videoControls.letterbox': return 'Letterbox'; - case 'videoControls.fillScreen': return 'Fill screen'; - case 'videoControls.stretch': return 'Stretch'; - case 'videoControls.lockRotation': return 'Lock rotation'; - case 'videoControls.unlockRotation': return 'Unlock rotation'; - case 'videoControls.sleepTimer': return 'Sleep Timer'; - case 'videoControls.timerActive': return 'Timer Active'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => 'Playback will pause in ${duration}'; - case 'videoControls.sleepTimerCompleted': return 'Sleep timer completed - playback paused'; - case 'videoControls.playButton': return 'Play'; - case 'videoControls.pauseButton': return 'Pause'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; - case 'videoControls.previousButton': return 'Previous episode'; - case 'videoControls.nextButton': return 'Next episode'; - case 'videoControls.previousChapterButton': return 'Previous chapter'; - case 'videoControls.nextChapterButton': return 'Next chapter'; - case 'videoControls.volumeButton': return 'Volume'; - case 'videoControls.muteButton': return 'Mute'; - case 'videoControls.unmuteButton': return 'Unmute'; - case 'videoControls.settingsButton': return 'Video settings'; - case 'videoControls.audioTrackButton': return 'Audio tracks'; - case 'videoControls.subtitlesButton': return 'Subtitles'; - case 'videoControls.chaptersButton': return 'Chapters'; - case 'videoControls.versionsButton': return 'Video versions'; - case 'videoControls.aspectRatioButton': return 'Aspect ratio'; - case 'videoControls.fullscreenButton': return 'Enter fullscreen'; - case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; - case 'videoControls.rotationLockButton': return 'Rotation lock'; - case 'videoControls.timelineSlider': return 'Video timeline'; - case 'videoControls.volumeSlider': return 'Volume level'; - case 'videoControls.backButton': return 'Back'; - case 'userStatus.admin': return 'Admin'; - case 'userStatus.restricted': return 'Restricted'; - case 'userStatus.protected': return 'Protected'; - case 'userStatus.current': return 'CURRENT'; - case 'messages.markedAsWatched': return 'Marked as watched'; - case 'messages.markedAsUnwatched': return 'Marked as unwatched'; - case 'messages.removedFromContinueWatching': return 'Removed from Continue Watching'; - case 'messages.errorLoading': return ({required Object error}) => 'Error: ${error}'; - case 'messages.fileInfoNotAvailable': return 'File information not available'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Error loading file info: ${error}'; - case 'messages.errorLoadingSeries': return 'Error loading series'; - case 'messages.errorLoadingSeason': return 'Error loading season'; - case 'messages.musicNotSupported': return 'Music playback is not yet supported'; - case 'messages.logsCleared': return 'Logs cleared'; - case 'messages.logsCopied': return 'Logs copied to clipboard'; - case 'messages.noLogsAvailable': return 'No logs available'; - case 'messages.libraryScanning': return ({required Object title}) => 'Scanning "${title}"...'; - case 'messages.libraryScanStarted': return ({required Object title}) => 'Library scan started for "${title}"'; - case 'messages.libraryScanFailed': return ({required Object error}) => 'Failed to scan library: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => 'Refreshing metadata for "${title}"...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => 'Metadata refresh started for "${title}"'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => 'Failed to refresh metadata: ${error}'; - case 'messages.noPlexToken': return 'No Plex token found. Please login again.'; - case 'messages.logoutConfirm': return 'Are you sure you want to logout?'; - case 'messages.noSeasonsFound': return 'No seasons found'; - case 'messages.noEpisodesFound': return 'No episodes found in first season'; - case 'messages.noEpisodesFoundGeneral': return 'No episodes found'; - case 'messages.noResultsFound': return 'No results found'; - case 'messages.sleepTimerSet': return ({required Object label}) => 'Sleep timer set for ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => 'Failed to switch to ${displayName}'; - case 'messages.noItemsAvailable': return 'No items available'; - case 'messages.failedToCreatePlayQueue': return 'Failed to create play queue'; - case 'messages.failedToCreatePlayQueueNoItems': return 'Failed to create play queue - no items'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => 'Failed to ${action}: ${error}'; - case 'profile.noUsersAvailable': return 'No users available'; - case 'subtitlingStyling.stylingOptions': return 'Styling Options'; - case 'subtitlingStyling.fontSize': return 'Font Size'; - case 'subtitlingStyling.textColor': return 'Text Color'; - case 'subtitlingStyling.borderSize': return 'Border Size'; - case 'subtitlingStyling.borderColor': return 'Border Color'; - case 'subtitlingStyling.backgroundOpacity': return 'Background Opacity'; - case 'subtitlingStyling.backgroundColor': return 'Background Color'; - case 'dialog.confirmAction': return 'Confirm Action'; - case 'dialog.areYouSure': return 'Are you sure you want to perform this action?'; - case 'dialog.cancel': return 'Cancel'; - case 'dialog.playNow': return 'Play Now'; - case 'discover.title': return 'Discover'; - case 'discover.switchProfile': return 'Switch Profile'; - case 'discover.logout': return 'Logout'; - case 'discover.noContentAvailable': return 'No content available'; - case 'discover.addMediaToLibraries': return 'Add some media to your libraries'; - case 'discover.continueWatching': return 'Continue Watching'; - case 'discover.recentlyAdded': return 'Recently Added'; - case 'discover.play': return 'Play'; - case 'discover.resume': return 'Resume'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => 'Play S${season}, E${episode}'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => 'Resume S${season}, E${episode}'; - case 'discover.pause': return 'Pause'; - case 'discover.overview': return 'Overview'; - case 'discover.cast': return 'Cast'; - case 'discover.seasons': return 'Seasons'; - case 'discover.studio': return 'Studio'; - case 'discover.rating': return 'Rating'; - case 'discover.watched': return 'Watched'; - case 'discover.episodeCount': return ({required Object count}) => '${count} episodes'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '${watched}/${total} watched'; - case 'discover.movie': return 'Movie'; - case 'discover.tvShow': return 'TV Show'; - case 'discover.minutesLeft': return ({required Object minutes}) => '${minutes} min left'; - case 'errors.searchFailed': return ({required Object error}) => 'Search failed: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => 'Connection timeout while loading ${context}'; - case 'errors.connectionFailed': return 'Unable to connect to Plex server'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => 'Failed to load ${context}: ${error}'; - case 'errors.noClientAvailable': return 'No client available'; - case 'errors.authenticationFailed': return ({required Object error}) => 'Authentication failed: ${error}'; - case 'errors.couldNotLaunchUrl': return 'Could not launch auth URL'; - case 'errors.pleaseEnterToken': return 'Please enter a token'; - case 'errors.invalidToken': return 'Invalid token'; - case 'errors.failedToVerifyToken': return ({required Object error}) => 'Failed to verify token: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => 'Failed to switch to ${displayName}'; - case 'errors.connectionFailedGeneric': return 'Connection failed'; - case 'libraries.title': return 'Libraries'; - case 'libraries.scanLibraryFiles': return 'Scan Library Files'; - case 'libraries.scanLibrary': return 'Scan Library'; - case 'libraries.analyze': return 'Analyze'; - case 'libraries.analyzeLibrary': return 'Analyze Library'; - case 'libraries.refreshMetadata': return 'Refresh Metadata'; - case 'libraries.emptyTrash': return 'Empty Trash'; - case 'libraries.emptyingTrash': return ({required Object title}) => 'Emptying trash for "${title}"...'; - case 'libraries.trashEmptied': return ({required Object title}) => 'Trash emptied for "${title}"'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => 'Failed to empty trash: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => 'Analyzing "${title}"...'; - case 'libraries.analysisStarted': return ({required Object title}) => 'Analysis started for "${title}"'; - case 'libraries.failedToAnalyze': return ({required Object error}) => 'Failed to analyze library: ${error}'; - case 'libraries.noLibrariesFound': return 'No libraries found'; - case 'libraries.thisLibraryIsEmpty': return 'This library is empty'; - case 'libraries.all': return 'All'; - case 'libraries.clearAll': return 'Clear All'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => 'Are you sure you want to scan "${title}"?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => 'Are you sure you want to analyze "${title}"?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => 'Are you sure you want to refresh metadata for "${title}"?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => 'Are you sure you want to empty trash for "${title}"?'; - case 'libraries.manageLibraries': return 'Manage Libraries'; - case 'libraries.sort': return 'Sort'; - case 'libraries.sortBy': return 'Sort By'; - case 'libraries.filters': return 'Filters'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => 'Loading library... (${count} items loaded)'; - case 'libraries.confirmActionMessage': return 'Are you sure you want to perform this action?'; - case 'libraries.showLibrary': return 'Show library'; - case 'libraries.hideLibrary': return 'Hide library'; - case 'libraries.libraryOptions': return 'Library options'; - case 'libraries.content': return 'library content'; - case 'libraries.selectLibrary': return 'Select library'; - case 'libraries.filtersWithCount': return ({required Object count}) => 'Filters (${count})'; - case 'libraries.noRecommendations': return 'No recommendations available'; - case 'libraries.noCollections': return 'No collections in this library'; - case 'libraries.noFoldersFound': return 'No folders found'; - case 'libraries.folders': return 'folders'; - case 'libraries.tabs.recommended': return 'Recommended'; - case 'libraries.tabs.browse': return 'Browse'; - case 'libraries.tabs.collections': return 'Collections'; - case 'libraries.tabs.playlists': return 'Playlists'; - case 'libraries.groupings.all': return 'All'; - case 'libraries.groupings.movies': return 'Movies'; - case 'libraries.groupings.shows': return 'TV Shows'; - case 'libraries.groupings.seasons': return 'Seasons'; - case 'libraries.groupings.episodes': return 'Episodes'; - case 'libraries.groupings.folders': return 'Folders'; - case 'about.title': return 'About'; - case 'about.openSourceLicenses': return 'Open Source Licenses'; - case 'about.versionLabel': return ({required Object version}) => 'Version ${version}'; - case 'about.appDescription': return 'A beautiful Plex client for Flutter'; - case 'about.viewLicensesDescription': return 'View licenses of third-party libraries'; - case 'serverSelection.connectingToServer': return 'Connecting to server...'; - case 'serverSelection.connectingToAllServers': return 'Connecting to all servers...'; - case 'serverSelection.connectToAllServers': return 'Connect to All Servers'; - case 'serverSelection.allServerConnectionsFailed': return 'Failed to connect to any servers. Please check your network and try again.'; - case 'serverSelection.foundServers': return ({required Object count}) => 'Found ${count} server(s)'; - case 'serverSelection.orSelectIndividually': return 'Or select a server individually:'; - case 'serverSelection.serverDebugCopied': return 'Server debug data copied to clipboard'; - case 'serverSelection.copyDebugData': return 'Copy Debug Data'; - case 'serverSelection.noServersFound': return 'No servers found'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => 'No servers found for ${username} (${email})'; - case 'serverSelection.malformedServerData': return ({required Object count}) => 'Found ${count} server(s) with malformed data. No valid servers available.'; - case 'serverSelection.incompleteServerInfo': return 'Some servers have incomplete information and were skipped. Please check your Plex.tv account.'; - case 'serverSelection.incompleteConnectionInfo': return 'Server connection information is incomplete. Please try again.'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => 'Server information is malformed: ${message}'; - case 'serverSelection.networkConnectionFailed': return 'Network connection failed. Please check your internet connection and try again.'; - case 'serverSelection.authenticationFailed': return 'Authentication failed. Please sign in again.'; - case 'serverSelection.plexServiceUnavailable': return 'Plex service unavailable. Please try again later.'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => 'Failed to load servers: ${error}'; - case 'hubDetail.title': return 'Title'; - case 'hubDetail.releaseYear': return 'Release Year'; - case 'hubDetail.dateAdded': return 'Date Added'; - case 'hubDetail.rating': return 'Rating'; - case 'hubDetail.noItemsFound': return 'No items found'; - case 'logs.title': return 'Logs'; - case 'logs.clearLogs': return 'Clear Logs'; - case 'logs.copyLogs': return 'Copy Logs'; - case 'logs.exportLogs': return 'Export Logs'; - case 'logs.noLogsToShow': return 'No logs to show'; - case 'logs.error': return 'Error:'; - case 'logs.stackTrace': return 'Stack Trace:'; - case 'licenses.relatedPackages': return 'Related Packages'; - case 'licenses.license': return 'License'; - case 'licenses.licenseNumber': return ({required Object number}) => 'License ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} licenses'; - case 'navigation.home': return 'Home'; - case 'navigation.search': return 'Search'; - case 'navigation.libraries': return 'Libraries'; - case 'navigation.settings': return 'Settings'; - case 'collections.title': return 'Collections'; - case 'collections.collection': return 'Collection'; - case 'collections.empty': return 'Collection is empty'; - case 'collections.noItems': return 'No items in this collection'; - case 'collections.unknownLibrarySection': return 'Cannot delete: Unknown library section'; - case 'collections.deleteCollection': return 'Delete Collection'; - case 'collections.deleteConfirm': return ({required Object title}) => 'Are you sure you want to delete "${title}"? This action cannot be undone.'; - case 'collections.deleted': return 'Collection deleted'; - case 'collections.deleteFailed': return 'Failed to delete collection'; - case 'collections.deleteFailedWithError': return ({required Object error}) => 'Failed to delete collection: ${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => 'Failed to load collection items: ${error}'; - case 'collections.addTo': return 'Add to collection'; - case 'collections.selectCollection': return 'Select Collection'; - case 'collections.createNewCollection': return 'Create New Collection'; - case 'collections.collectionName': return 'Collection Name'; - case 'collections.enterCollectionName': return 'Enter collection name'; - case 'collections.addedToCollection': return 'Added to collection'; - case 'collections.errorAddingToCollection': return 'Failed to add to collection'; - case 'collections.created': return 'Collection created'; - case 'collections.removeFromCollection': return 'Remove from collection'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => 'Remove "${title}" from this collection?'; - case 'collections.removedFromCollection': return 'Removed from collection'; - case 'collections.removeFromCollectionFailed': return 'Failed to remove from collection'; - case 'collections.removeFromCollectionError': return ({required Object error}) => 'Error removing from collection: ${error}'; - case 'playlists.title': return 'Playlists'; - case 'playlists.playlist': return 'Playlist'; - case 'playlists.noPlaylists': return 'No playlists found'; - case 'playlists.create': return 'Create Playlist'; - case 'playlists.newPlaylist': return 'New Playlist'; - case 'playlists.playlistName': return 'Playlist Name'; - case 'playlists.enterPlaylistName': return 'Enter playlist name'; - case 'playlists.edit': return 'Edit Playlist'; - case 'playlists.delete': return 'Delete Playlist'; - case 'playlists.addTo': return 'Add to Playlist'; - case 'playlists.addItems': return 'Add Items'; - case 'playlists.removeItem': return 'Remove from Playlist'; - case 'playlists.clearPlaylist': return 'Clear Playlist'; - case 'playlists.playAll': return 'Play All'; - case 'playlists.shuffle': return 'Shuffle'; - case 'playlists.smartPlaylist': return 'Smart Playlist'; - case 'playlists.regularPlaylist': return 'Regular Playlist'; - case 'playlists.itemCount': return ({required Object count}) => '${count} items'; - case 'playlists.oneItem': return '1 item'; - case 'playlists.emptyPlaylist': return 'This playlist is empty'; - case 'playlists.deleteConfirm': return 'Delete Playlist?'; - case 'playlists.deleteMessage': return ({required Object name}) => 'Are you sure you want to delete "${name}"?'; - case 'playlists.created': return 'Playlist created'; - case 'playlists.updated': return 'Playlist updated'; - case 'playlists.deleted': return 'Playlist deleted'; - case 'playlists.itemAdded': return 'Added to playlist'; - case 'playlists.itemRemoved': return 'Removed from playlist'; - case 'playlists.selectPlaylist': return 'Select Playlist'; - case 'playlists.createNewPlaylist': return 'Create New Playlist'; - case 'playlists.errorCreating': return 'Failed to create playlist'; - case 'playlists.errorDeleting': return 'Failed to delete playlist'; - case 'playlists.errorLoading': return 'Failed to load playlists'; - case 'playlists.errorAdding': return 'Failed to add to playlist'; - case 'playlists.errorReordering': return 'Failed to reorder playlist item'; - case 'playlists.errorRemoving': return 'Failed to remove from playlist'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return 'Loading...'; + case 'auth.signInWithPlex': + return 'Sign in with Plex'; + case 'auth.showQRCode': + return 'Show QR Code'; + case 'auth.cancel': + return 'Cancel'; + case 'auth.authenticate': + return 'Authenticate'; + case 'auth.retry': + return 'Retry'; + case 'auth.debugEnterToken': + return 'Debug: Enter Plex Token'; + case 'auth.plexTokenLabel': + return 'Plex Auth Token'; + case 'auth.plexTokenHint': + return 'Enter your Plex.tv token'; + case 'auth.authenticationTimeout': + return 'Authentication timed out. Please try again.'; + case 'auth.scanQRCodeInstruction': + return 'Scan this QR code with a device logged into Plex to authenticate.'; + case 'auth.waitingForAuth': + return 'Waiting for authentication...\nPlease complete sign-in in your browser.'; + case 'common.cancel': + return 'Cancel'; + case 'common.save': + return 'Save'; + case 'common.close': + return 'Close'; + case 'common.clear': + return 'Clear'; + case 'common.reset': + return 'Reset'; + case 'common.later': + return 'Later'; + case 'common.submit': + return 'Submit'; + case 'common.confirm': + return 'Confirm'; + case 'common.retry': + return 'Retry'; + case 'common.playNow': + return 'Play Now'; + case 'common.logout': + return 'Logout'; + case 'common.online': + return 'Online'; + case 'common.offline': + return 'Offline'; + case 'common.owned': + return 'Owned'; + case 'common.shared': + return 'Shared'; + case 'common.current': + return 'CURRENT'; + case 'common.unknown': + return 'Unknown'; + case 'common.refresh': + return 'Refresh'; + case 'common.yes': + return 'Yes'; + case 'common.no': + return 'No'; + case 'common.server': + return 'Server'; + case 'common.delete': + return 'Delete'; + case 'common.shuffle': + return 'Shuffle'; + case 'common.addTo': + return 'Add to...'; + case 'screens.licenses': + return 'Licenses'; + case 'screens.switchProfile': + return 'Switch Profile'; + case 'screens.subtitleStyling': + return 'Subtitle Styling'; + case 'screens.search': + return 'Search'; + case 'screens.logs': + return 'Logs'; + case 'update.available': + return 'Update Available'; + case 'update.versionAvailable': + return ({required Object version}) => 'Version ${version} is available'; + case 'update.currentVersion': + return ({required Object version}) => 'Current: ${version}'; + case 'update.skipVersion': + return 'Skip This Version'; + case 'update.viewRelease': + return 'View Release'; + case 'update.latestVersion': + return 'You are on the latest version'; + case 'update.checkFailed': + return 'Failed to check for updates'; + case 'settings.title': + return 'Settings'; + case 'settings.language': + return 'Language'; + case 'settings.theme': + return 'Theme'; + case 'settings.appearance': + return 'Appearance'; + case 'settings.videoPlayback': + return 'Video Playback'; + case 'settings.shufflePlay': + return 'Shuffle Play'; + case 'settings.advanced': + return 'Advanced'; + case 'settings.useSeasonPostersDescription': + return 'Show season poster instead of series poster for episodes'; + case 'settings.showHeroSectionDescription': + return 'Display featured content carousel on home screen'; + case 'settings.secondsLabel': + return 'Seconds'; + case 'settings.minutesLabel': + return 'Minutes'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + 'Enter duration (${min}-${max})'; + case 'settings.systemTheme': + return 'System'; + case 'settings.systemThemeDescription': + return 'Follow system settings'; + case 'settings.lightTheme': + return 'Light'; + case 'settings.darkTheme': + return 'Dark'; + case 'settings.libraryDensity': + return 'Library Density'; + case 'settings.compact': + return 'Compact'; + case 'settings.compactDescription': + return 'Smaller cards, more items visible'; + case 'settings.normal': + return 'Normal'; + case 'settings.normalDescription': + return 'Default size'; + case 'settings.comfortable': + return 'Comfortable'; + case 'settings.comfortableDescription': + return 'Larger cards, fewer items visible'; + case 'settings.viewMode': + return 'View Mode'; + case 'settings.gridView': + return 'Grid'; + case 'settings.gridViewDescription': + return 'Display items in a grid layout'; + case 'settings.listView': + return 'List'; + case 'settings.listViewDescription': + return 'Display items in a list layout'; + case 'settings.useSeasonPosters': + return 'Use Season Posters'; + case 'settings.showHeroSection': + return 'Show Hero Section'; + case 'settings.hardwareDecoding': + return 'Hardware Decoding'; + case 'settings.hardwareDecodingDescription': + return 'Use hardware acceleration when available'; + case 'settings.bufferSize': + return 'Buffer Size'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return 'Subtitle Styling'; + case 'settings.subtitleStylingDescription': + return 'Customize subtitle appearance'; + case 'settings.smallSkipDuration': + return 'Small Skip Duration'; + case 'settings.largeSkipDuration': + return 'Large Skip Duration'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} seconds'; + case 'settings.defaultSleepTimer': + return 'Default Sleep Timer'; + case 'settings.minutesUnit': + return ({required Object minutes}) => '${minutes} minutes'; + case 'settings.rememberTrackSelections': + return 'Remember track selections per show/movie'; + case 'settings.rememberTrackSelectionsDescription': + return 'Automatically save audio and subtitle language preferences when you change tracks during playback'; + case 'settings.unwatchedOnly': + return 'Unwatched Only'; + case 'settings.unwatchedOnlyDescription': + return 'Only include unwatched episodes in shuffle queue'; + case 'settings.shuffleOrderNavigation': + return 'Shuffle Order Navigation'; + case 'settings.shuffleOrderNavigationDescription': + return 'Next/previous buttons follow shuffled order'; + case 'settings.loopShuffleQueue': + return 'Loop Shuffle Queue'; + case 'settings.loopShuffleQueueDescription': + return 'Restart queue when reaching the end'; + case 'settings.videoPlayerControls': + return 'Video Player Controls'; + case 'settings.keyboardShortcuts': + return 'Keyboard Shortcuts'; + case 'settings.keyboardShortcutsDescription': + return 'Customize keyboard shortcuts'; + case 'settings.debugLogging': + return 'Debug Logging'; + case 'settings.debugLoggingDescription': + return 'Enable detailed logging for troubleshooting'; + case 'settings.viewLogs': + return 'View Logs'; + case 'settings.viewLogsDescription': + return 'View application logs'; + case 'settings.clearCache': + return 'Clear Cache'; + case 'settings.clearCacheDescription': + return 'This will clear all cached images and data. The app may take longer to load content after clearing the cache.'; + case 'settings.clearCacheSuccess': + return 'Cache cleared successfully'; + case 'settings.resetSettings': + return 'Reset Settings'; + case 'settings.resetSettingsDescription': + return 'This will reset all settings to their default values. This action cannot be undone.'; + case 'settings.resetSettingsSuccess': + return 'Settings reset successfully'; + case 'settings.shortcutsReset': + return 'Shortcuts reset to defaults'; + case 'settings.about': + return 'About'; + case 'settings.aboutDescription': + return 'App information and licenses'; + case 'settings.updates': + return 'Updates'; + case 'settings.updateAvailable': + return 'Update Available'; + case 'settings.checkForUpdates': + return 'Check for Updates'; + case 'settings.validationErrorEnterNumber': + return 'Please enter a valid number'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => 'Duration must be between ${min} and ${max} ${unit}'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => + 'Shortcut already assigned to ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => 'Shortcut updated for ${action}'; + case 'settings.autoSkip': + return 'Auto Skip'; + case 'settings.autoSkipIntro': + return 'Auto Skip Intro'; + case 'settings.autoSkipIntroDescription': + return 'Automatically skip intro markers after a few seconds'; + case 'settings.autoSkipCredits': + return 'Auto Skip Credits'; + case 'settings.autoSkipCreditsDescription': + return 'Automatically skip credits and play next episode'; + case 'settings.autoSkipDelay': + return 'Auto Skip Delay'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => + 'Wait ${seconds} seconds before auto-skipping'; + case 'settings.downloads': + return 'Downloads'; + case 'settings.downloadLocationDescription': + return 'Choose where to store downloaded content'; + case 'settings.downloadLocationDefault': + return 'Default (App Storage)'; + case 'settings.downloadLocationCustom': + return 'Custom Location'; + case 'settings.selectFolder': + return 'Select Folder'; + case 'settings.resetToDefault': + return 'Reset to Default'; + case 'settings.currentPath': + return ({required Object path}) => 'Current: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'Custom download location is not supported on iOS'; + case 'settings.downloadLocationChanged': + return 'Download location changed'; + case 'settings.downloadLocationReset': + return 'Download location reset to default'; + case 'settings.downloadLocationInvalid': + return 'Selected folder is not writable'; + case 'settings.downloadLocationSelectError': + return 'Failed to select folder'; + case 'settings.downloadOnWifiOnly': + return 'Download on WiFi only'; + case 'settings.downloadOnWifiOnlyDescription': + return 'Prevent downloads when on cellular data'; + case 'settings.cellularDownloadBlocked': + return 'Downloads are disabled on cellular data. Connect to WiFi or change the setting.'; + case 'search.hint': + return 'Search movies, shows, music...'; + case 'search.tryDifferentTerm': + return 'Try a different search term'; + case 'search.searchYourMedia': + return 'Search your media'; + case 'search.enterTitleActorOrKeyword': + return 'Enter a title, actor, or keyword'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => + 'Set Shortcut for ${actionName}'; + case 'hotkeys.clearShortcut': + return 'Clear shortcut'; + case 'pinEntry.enterPin': + return 'Enter PIN'; + case 'pinEntry.showPin': + return 'Show PIN'; + case 'pinEntry.hidePin': + return 'Hide PIN'; + case 'fileInfo.title': + return 'File Info'; + case 'fileInfo.video': + return 'Video'; + case 'fileInfo.audio': + return 'Audio'; + case 'fileInfo.file': + return 'File'; + case 'fileInfo.advanced': + return 'Advanced'; + case 'fileInfo.codec': + return 'Codec'; + case 'fileInfo.resolution': + return 'Resolution'; + case 'fileInfo.bitrate': + return 'Bitrate'; + case 'fileInfo.frameRate': + return 'Frame Rate'; + case 'fileInfo.aspectRatio': + return 'Aspect Ratio'; + case 'fileInfo.profile': + return 'Profile'; + case 'fileInfo.bitDepth': + return 'Bit Depth'; + case 'fileInfo.colorSpace': + return 'Color Space'; + case 'fileInfo.colorRange': + return 'Color Range'; + case 'fileInfo.colorPrimaries': + return 'Color Primaries'; + case 'fileInfo.chromaSubsampling': + return 'Chroma Subsampling'; + case 'fileInfo.channels': + return 'Channels'; + case 'fileInfo.path': + return 'Path'; + case 'fileInfo.size': + return 'Size'; + case 'fileInfo.container': + return 'Container'; + case 'fileInfo.duration': + return 'Duration'; + case 'fileInfo.optimizedForStreaming': + return 'Optimized for Streaming'; + case 'fileInfo.has64bitOffsets': + return '64-bit Offsets'; + case 'mediaMenu.markAsWatched': + return 'Mark as Watched'; + case 'mediaMenu.markAsUnwatched': + return 'Mark as Unwatched'; + case 'mediaMenu.removeFromContinueWatching': + return 'Remove from Continue Watching'; + case 'mediaMenu.goToSeries': + return 'Go to series'; + case 'mediaMenu.goToSeason': + return 'Go to season'; + case 'mediaMenu.shufflePlay': + return 'Shuffle Play'; + case 'mediaMenu.fileInfo': + return 'File Info'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': + return 'unwatched'; + case 'accessibility.tapToPlay': + return 'Tap to play'; + case 'tooltips.shufflePlay': + return 'Shuffle play'; + case 'tooltips.markAsWatched': + return 'Mark as watched'; + case 'tooltips.markAsUnwatched': + return 'Mark as unwatched'; + case 'videoControls.audioLabel': + return 'Audio'; + case 'videoControls.subtitlesLabel': + return 'Subtitles'; + case 'videoControls.resetToZero': + return 'Reset to 0ms'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} plays later'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} plays earlier'; + case 'videoControls.noOffset': + return 'No offset'; + case 'videoControls.letterbox': + return 'Letterbox'; + case 'videoControls.fillScreen': + return 'Fill screen'; + case 'videoControls.stretch': + return 'Stretch'; + case 'videoControls.lockRotation': + return 'Lock rotation'; + case 'videoControls.unlockRotation': + return 'Unlock rotation'; + case 'videoControls.sleepTimer': + return 'Sleep Timer'; + case 'videoControls.timerActive': + return 'Timer Active'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => + 'Playback will pause in ${duration}'; + case 'videoControls.sleepTimerCompleted': + return 'Sleep timer completed - playback paused'; + case 'videoControls.playButton': + return 'Play'; + case 'videoControls.pauseButton': + return 'Pause'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => + 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': + return 'Previous episode'; + case 'videoControls.nextButton': + return 'Next episode'; + case 'videoControls.previousChapterButton': + return 'Previous chapter'; + case 'videoControls.nextChapterButton': + return 'Next chapter'; + case 'videoControls.volumeButton': + return 'Volume'; + case 'videoControls.muteButton': + return 'Mute'; + case 'videoControls.unmuteButton': + return 'Unmute'; + case 'videoControls.settingsButton': + return 'Video settings'; + case 'videoControls.audioTrackButton': + return 'Audio tracks'; + case 'videoControls.subtitlesButton': + return 'Subtitles'; + case 'videoControls.chaptersButton': + return 'Chapters'; + case 'videoControls.versionsButton': + return 'Video versions'; + case 'videoControls.aspectRatioButton': + return 'Aspect ratio'; + case 'videoControls.fullscreenButton': + return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': + return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': + return 'Rotation lock'; + case 'videoControls.timelineSlider': + return 'Video timeline'; + case 'videoControls.volumeSlider': + return 'Volume level'; + case 'videoControls.backButton': + return 'Back'; + case 'userStatus.admin': + return 'Admin'; + case 'userStatus.restricted': + return 'Restricted'; + case 'userStatus.protected': + return 'Protected'; + case 'userStatus.current': + return 'CURRENT'; + case 'messages.markedAsWatched': + return 'Marked as watched'; + case 'messages.markedAsUnwatched': + return 'Marked as unwatched'; + case 'messages.markedAsWatchedOffline': + return 'Marked as watched (will sync when online)'; + case 'messages.markedAsUnwatchedOffline': + return 'Marked as unwatched (will sync when online)'; + case 'messages.removedFromContinueWatching': + return 'Removed from Continue Watching'; + case 'messages.errorLoading': + return ({required Object error}) => 'Error: ${error}'; + case 'messages.fileInfoNotAvailable': + return 'File information not available'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => 'Error loading file info: ${error}'; + case 'messages.errorLoadingSeries': + return 'Error loading series'; + case 'messages.errorLoadingSeason': + return 'Error loading season'; + case 'messages.musicNotSupported': + return 'Music playback is not yet supported'; + case 'messages.logsCleared': + return 'Logs cleared'; + case 'messages.logsCopied': + return 'Logs copied to clipboard'; + case 'messages.noLogsAvailable': + return 'No logs available'; + case 'messages.libraryScanning': + return ({required Object title}) => 'Scanning "${title}"...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => + 'Library scan started for "${title}"'; + case 'messages.libraryScanFailed': + return ({required Object error}) => 'Failed to scan library: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => + 'Refreshing metadata for "${title}"...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => + 'Metadata refresh started for "${title}"'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => + 'Failed to refresh metadata: ${error}'; + case 'messages.noPlexToken': + return 'No Plex token found. Please login again.'; + case 'messages.logoutConfirm': + return 'Are you sure you want to logout?'; + case 'messages.noSeasonsFound': + return 'No seasons found'; + case 'messages.noEpisodesFound': + return 'No episodes found in first season'; + case 'messages.noEpisodesFoundGeneral': + return 'No episodes found'; + case 'messages.noResultsFound': + return 'No results found'; + case 'messages.sleepTimerSet': + return ({required Object label}) => 'Sleep timer set for ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => + 'Failed to switch to ${displayName}'; + case 'messages.noItemsAvailable': + return 'No items available'; + case 'messages.failedToCreatePlayQueue': + return 'Failed to create play queue'; + case 'messages.failedToCreatePlayQueueNoItems': + return 'Failed to create play queue - no items'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + 'Failed to ${action}: ${error}'; + case 'messages.youAreOffline': + return 'You\'re Offline'; + case 'messages.offlineFeatureUnavailable': + return 'This feature requires an internet connection. Your downloads and settings are still available.'; + case 'profile.noUsersAvailable': + return 'No users available'; + case 'subtitlingStyling.stylingOptions': + return 'Styling Options'; + case 'subtitlingStyling.fontSize': + return 'Font Size'; + case 'subtitlingStyling.textColor': + return 'Text Color'; + case 'subtitlingStyling.borderSize': + return 'Border Size'; + case 'subtitlingStyling.borderColor': + return 'Border Color'; + case 'subtitlingStyling.backgroundOpacity': + return 'Background Opacity'; + case 'subtitlingStyling.backgroundColor': + return 'Background Color'; + case 'dialog.confirmAction': + return 'Confirm Action'; + case 'dialog.areYouSure': + return 'Are you sure you want to perform this action?'; + case 'dialog.cancel': + return 'Cancel'; + case 'dialog.playNow': + return 'Play Now'; + case 'discover.title': + return 'Discover'; + case 'discover.switchProfile': + return 'Switch Profile'; + case 'discover.logout': + return 'Logout'; + case 'discover.noContentAvailable': + return 'No content available'; + case 'discover.addMediaToLibraries': + return 'Add some media to your libraries'; + case 'discover.continueWatching': + return 'Continue Watching'; + case 'discover.recentlyAdded': + return 'Recently Added'; + case 'discover.play': + return 'Play'; + case 'discover.resume': + return 'Resume'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return 'Pause'; + case 'discover.overview': + return 'Overview'; + case 'discover.cast': + return 'Cast'; + case 'discover.seasons': + return 'Seasons'; + case 'discover.studio': + return 'Studio'; + case 'discover.rating': + return 'Rating'; + case 'discover.watched': + return 'Watched'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} episodes'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '${watched}/${total} watched'; + case 'discover.movie': + return 'Movie'; + case 'discover.tvShow': + return 'TV Show'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '${minutes} min left'; + case 'errors.searchFailed': + return ({required Object error}) => 'Search failed: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => + 'Connection timeout while loading ${context}'; + case 'errors.connectionFailed': + return 'Unable to connect to Plex server'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + 'Failed to load ${context}: ${error}'; + case 'errors.noClientAvailable': + return 'No client available'; + case 'errors.authenticationFailed': + return ({required Object error}) => 'Authentication failed: ${error}'; + case 'errors.couldNotLaunchUrl': + return 'Could not launch auth URL'; + case 'errors.pleaseEnterToken': + return 'Please enter a token'; + case 'errors.invalidToken': + return 'Invalid token'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => 'Failed to verify token: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => + 'Failed to switch to ${displayName}'; + case 'errors.connectionFailedGeneric': + return 'Connection failed'; + case 'libraries.title': + return 'Libraries'; + case 'libraries.scanLibraryFiles': + return 'Scan Library Files'; + case 'libraries.scanLibrary': + return 'Scan Library'; + case 'libraries.analyze': + return 'Analyze'; + case 'libraries.analyzeLibrary': + return 'Analyze Library'; + case 'libraries.refreshMetadata': + return 'Refresh Metadata'; + case 'libraries.emptyTrash': + return 'Empty Trash'; + case 'libraries.emptyingTrash': + return ({required Object title}) => 'Emptying trash for "${title}"...'; + case 'libraries.trashEmptied': + return ({required Object title}) => 'Trash emptied for "${title}"'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => 'Failed to empty trash: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => 'Analyzing "${title}"...'; + case 'libraries.analysisStarted': + return ({required Object title}) => 'Analysis started for "${title}"'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => + 'Failed to analyze library: ${error}'; + case 'libraries.noLibrariesFound': + return 'No libraries found'; + case 'libraries.thisLibraryIsEmpty': + return 'This library is empty'; + case 'libraries.all': + return 'All'; + case 'libraries.clearAll': + return 'Clear All'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => + 'Are you sure you want to scan "${title}"?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => + 'Are you sure you want to analyze "${title}"?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => + 'Are you sure you want to refresh metadata for "${title}"?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => + 'Are you sure you want to empty trash for "${title}"?'; + case 'libraries.manageLibraries': + return 'Manage Libraries'; + case 'libraries.sort': + return 'Sort'; + case 'libraries.sortBy': + return 'Sort By'; + case 'libraries.filters': + return 'Filters'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => + 'Loading library... (${count} items loaded)'; + case 'libraries.confirmActionMessage': + return 'Are you sure you want to perform this action?'; + case 'libraries.showLibrary': + return 'Show library'; + case 'libraries.hideLibrary': + return 'Hide library'; + case 'libraries.libraryOptions': + return 'Library options'; + case 'libraries.content': + return 'library content'; + case 'libraries.selectLibrary': + return 'Select library'; + case 'libraries.filtersWithCount': + return ({required Object count}) => 'Filters (${count})'; + case 'libraries.noRecommendations': + return 'No recommendations available'; + case 'libraries.noCollections': + return 'No collections in this library'; + case 'libraries.noFoldersFound': + return 'No folders found'; + case 'libraries.folders': + return 'folders'; + case 'libraries.tabs.recommended': + return 'Recommended'; + case 'libraries.tabs.browse': + return 'Browse'; + case 'libraries.tabs.collections': + return 'Collections'; + case 'libraries.tabs.playlists': + return 'Playlists'; + case 'libraries.groupings.all': + return 'All'; + case 'libraries.groupings.movies': + return 'Movies'; + case 'libraries.groupings.shows': + return 'TV Shows'; + case 'libraries.groupings.seasons': + return 'Seasons'; + case 'libraries.groupings.episodes': + return 'Episodes'; + case 'libraries.groupings.folders': + return 'Folders'; + case 'about.title': + return 'About'; + case 'about.openSourceLicenses': + return 'Open Source Licenses'; + case 'about.versionLabel': + return ({required Object version}) => 'Version ${version}'; + case 'about.appDescription': + return 'A beautiful Plex client for Flutter'; + case 'about.viewLicensesDescription': + return 'View licenses of third-party libraries'; + case 'serverSelection.connectingToServer': + return 'Connecting to server...'; + case 'serverSelection.connectingToAllServers': + return 'Connecting to all servers...'; + case 'serverSelection.connectToAllServers': + return 'Connect to All Servers'; + case 'serverSelection.allServerConnectionsFailed': + return 'Failed to connect to any servers. Please check your network and try again.'; + case 'serverSelection.foundServers': + return ({required Object count}) => 'Found ${count} server(s)'; + case 'serverSelection.orSelectIndividually': + return 'Or select a server individually:'; + case 'serverSelection.serverDebugCopied': + return 'Server debug data copied to clipboard'; + case 'serverSelection.copyDebugData': + return 'Copy Debug Data'; + case 'serverSelection.noServersFound': + return 'No servers found'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + 'No servers found for ${username} (${email})'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + 'Found ${count} server(s) with malformed data. No valid servers available.'; + case 'serverSelection.incompleteServerInfo': + return 'Some servers have incomplete information and were skipped. Please check your Plex.tv account.'; + case 'serverSelection.incompleteConnectionInfo': + return 'Server connection information is incomplete. Please try again.'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => + 'Server information is malformed: ${message}'; + case 'serverSelection.networkConnectionFailed': + return 'Network connection failed. Please check your internet connection and try again.'; + case 'serverSelection.authenticationFailed': + return 'Authentication failed. Please sign in again.'; + case 'serverSelection.plexServiceUnavailable': + return 'Plex service unavailable. Please try again later.'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => 'Failed to load servers: ${error}'; + case 'hubDetail.title': + return 'Title'; + case 'hubDetail.releaseYear': + return 'Release Year'; + case 'hubDetail.dateAdded': + return 'Date Added'; + case 'hubDetail.rating': + return 'Rating'; + case 'hubDetail.noItemsFound': + return 'No items found'; + case 'logs.title': + return 'Logs'; + case 'logs.clearLogs': + return 'Clear Logs'; + case 'logs.copyLogs': + return 'Copy Logs'; + case 'logs.exportLogs': + return 'Export Logs'; + case 'logs.noLogsToShow': + return 'No logs to show'; + case 'logs.error': + return 'Error:'; + case 'logs.stackTrace': + return 'Stack Trace:'; + case 'licenses.relatedPackages': + return 'Related Packages'; + case 'licenses.license': + return 'License'; + case 'licenses.licenseNumber': + return ({required Object number}) => 'License ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} licenses'; + case 'navigation.home': + return 'Home'; + case 'navigation.search': + return 'Search'; + case 'navigation.libraries': + return 'Libraries'; + case 'navigation.settings': + return 'Settings'; + case 'navigation.downloads': + return 'Downloads'; + case 'collections.title': + return 'Collections'; + case 'collections.collection': + return 'Collection'; + case 'collections.empty': + return 'Collection is empty'; + case 'collections.noItems': + return 'No items in this collection'; + case 'collections.unknownLibrarySection': + return 'Cannot delete: Unknown library section'; + case 'collections.deleteCollection': + return 'Delete Collection'; + case 'collections.deleteConfirm': + return ({required Object title}) => + 'Are you sure you want to delete "${title}"? This action cannot be undone.'; + case 'collections.deleted': + return 'Collection deleted'; + case 'collections.deleteFailed': + return 'Failed to delete collection'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => + 'Failed to delete collection: ${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => + 'Failed to load collection items: ${error}'; + case 'collections.addTo': + return 'Add to collection'; + case 'collections.selectCollection': + return 'Select Collection'; + case 'collections.createNewCollection': + return 'Create New Collection'; + case 'collections.collectionName': + return 'Collection Name'; + case 'collections.enterCollectionName': + return 'Enter collection name'; + case 'collections.addedToCollection': + return 'Added to collection'; + case 'collections.errorAddingToCollection': + return 'Failed to add to collection'; + case 'collections.created': + return 'Collection created'; + case 'collections.removeFromCollection': + return 'Remove from collection'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => + 'Remove "${title}" from this collection?'; + case 'collections.removedFromCollection': + return 'Removed from collection'; + case 'collections.removeFromCollectionFailed': + return 'Failed to remove from collection'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => + 'Error removing from collection: ${error}'; + case 'playlists.title': + return 'Playlists'; + case 'playlists.playlist': + return 'Playlist'; + case 'playlists.noPlaylists': + return 'No playlists found'; + case 'playlists.create': + return 'Create Playlist'; + case 'playlists.newPlaylist': + return 'New Playlist'; + case 'playlists.playlistName': + return 'Playlist Name'; + case 'playlists.enterPlaylistName': + return 'Enter playlist name'; + case 'playlists.edit': + return 'Edit Playlist'; + case 'playlists.delete': + return 'Delete Playlist'; + case 'playlists.addTo': + return 'Add to Playlist'; + case 'playlists.addItems': + return 'Add Items'; + case 'playlists.removeItem': + return 'Remove from Playlist'; + case 'playlists.clearPlaylist': + return 'Clear Playlist'; + case 'playlists.playAll': + return 'Play All'; + case 'playlists.shuffle': + return 'Shuffle'; + case 'playlists.smartPlaylist': + return 'Smart Playlist'; + case 'playlists.regularPlaylist': + return 'Regular Playlist'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} items'; + case 'playlists.oneItem': + return '1 item'; + case 'playlists.emptyPlaylist': + return 'This playlist is empty'; + case 'playlists.deleteConfirm': + return 'Delete Playlist?'; + case 'playlists.deleteMessage': + return ({required Object name}) => + 'Are you sure you want to delete "${name}"?'; + case 'playlists.created': + return 'Playlist created'; + case 'playlists.updated': + return 'Playlist updated'; + case 'playlists.deleted': + return 'Playlist deleted'; + case 'playlists.itemAdded': + return 'Added to playlist'; + case 'playlists.itemRemoved': + return 'Removed from playlist'; + case 'playlists.selectPlaylist': + return 'Select Playlist'; + case 'playlists.createNewPlaylist': + return 'Create New Playlist'; + case 'playlists.errorCreating': + return 'Failed to create playlist'; + case 'playlists.errorDeleting': + return 'Failed to delete playlist'; + case 'playlists.errorLoading': + return 'Failed to load playlists'; + case 'playlists.errorAdding': + return 'Failed to add to playlist'; + case 'playlists.errorReordering': + return 'Failed to reorder playlist item'; + case 'playlists.errorRemoving': + return 'Failed to remove from playlist'; + case 'downloads.title': + return 'Downloads'; + case 'downloads.manage': + return 'Manage'; + case 'downloads.tvShows': + return 'TV Shows'; + case 'downloads.movies': + return 'Movies'; + case 'downloads.queue': + return 'Queue'; + case 'downloads.noDownloads': + return 'No downloads yet'; + case 'downloads.noDownloadsDescription': + return 'Downloaded content will appear here for offline viewing'; + case 'downloads.browseContent': + return 'Browse content'; + case 'downloads.downloading': + return 'Downloading'; + case 'downloads.downloaded': + return 'Downloaded'; + case 'downloads.paused': + return 'Paused'; + case 'downloads.failed': + return 'Failed'; + case 'downloads.queued': + return 'Queued'; + case 'downloads.downloadNow': + return 'Download'; + case 'downloads.deleteDownload': + return 'Delete download'; + case 'downloads.cancelDownload': + return 'Cancel download'; + case 'downloads.pauseDownload': + return 'Pause download'; + case 'downloads.resumeDownload': + return 'Resume download'; + case 'downloads.retryDownload': + return 'Retry download'; + case 'downloads.downloadComplete': + return 'Download complete'; + case 'downloads.downloadFailed': + return 'Download failed'; + case 'downloads.downloadCancelled': + return 'Download cancelled'; + case 'downloads.downloadQueued': + return 'Download queued'; + case 'downloads.episodesQueued': + return ({required Object count}) => + '${count} episodes queued for download'; + case 'downloads.downloadDeleted': + return 'Download deleted'; + case 'downloads.deleteConfirmTitle': + return 'Delete download?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + 'Are you sure you want to delete "${title}"? This will remove the downloaded file from your device.'; + case 'downloads.deleteConfirmMessage': + return 'This will remove the downloaded file from your device.'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => 'Deleting ${title}... (${current} of ${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => 'Deleting ${title}...'; + case 'downloads.storageUsed': + return 'Storage used'; + case 'downloads.availableOffline': + return 'Available offline'; + case 'downloads.downloadingSeason': + return 'Downloading season'; + case 'downloads.downloadingEpisode': + return 'Downloading episode'; + case 'offline.youAreOffline': + return 'You\'re offline'; + case 'offline.offlineMode': + return 'Offline mode'; + case 'offline.backOnline': + return 'Back online'; + case 'offline.showingDownloadedContent': + return 'Showing downloaded content only'; + case 'offline.noInternetConnection': + return 'No internet connection'; + case 'offline.serversUnreachable': + return 'Plex servers unreachable'; + case 'offline.connectToDownload': + return 'Connect to the internet to download content'; + default: + return null; + } + } } extension on _StringsDe { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return 'Lädt...'; - case 'auth.signInWithPlex': return 'Mit Plex anmelden'; - case 'auth.showQRCode': return 'QR-Code anzeigen'; - case 'auth.cancel': return 'Abbrechen'; - case 'auth.authenticate': return 'Authentifizieren'; - case 'auth.retry': return 'Erneut versuchen'; - case 'auth.debugEnterToken': return 'Debug: Plex-Token eingeben'; - case 'auth.plexTokenLabel': return 'Plex-Auth-Token'; - case 'auth.plexTokenHint': return 'Plex.tv-Token eingeben'; - case 'auth.authenticationTimeout': return 'Authentifizierung abgelaufen. Bitte erneut versuchen.'; - case 'auth.scanQRCodeInstruction': return 'Diesen QR-Code mit einem bei Plex angemeldeten Gerät scannen, um zu authentifizieren.'; - case 'auth.waitingForAuth': return 'Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.'; - case 'common.cancel': return 'Abbrechen'; - case 'common.save': return 'Speichern'; - case 'common.close': return 'Schließen'; - case 'common.clear': return 'Leeren'; - case 'common.reset': return 'Zurücksetzen'; - case 'common.later': return 'Später'; - case 'common.submit': return 'Senden'; - case 'common.confirm': return 'Bestätigen'; - case 'common.retry': return 'Erneut versuchen'; - case 'common.playNow': return 'Jetzt abspielen'; - case 'common.logout': return 'Abmelden'; - case 'common.online': return 'Online'; - case 'common.offline': return 'Offline'; - case 'common.owned': return 'Besitzer'; - case 'common.shared': return 'Geteilt'; - case 'common.current': return 'AKTUELL'; - case 'common.unknown': return 'Unbekannt'; - case 'common.refresh': return 'Aktualisieren'; - case 'common.yes': return 'Ja'; - case 'common.no': return 'Nein'; - case 'common.server': return 'Server'; - case 'common.delete': return 'Löschen'; - case 'common.shuffle': return 'Zufall'; - case 'common.addTo': return 'Hinzufügen zu...'; - case 'screens.licenses': return 'Lizenzen'; - case 'screens.selectServer': return 'Server auswählen'; - case 'screens.switchProfile': return 'Profil wechseln'; - case 'screens.subtitleStyling': return 'Untertitel-Stil'; - case 'screens.search': return 'Suche'; - case 'screens.logs': return 'Protokolle'; - case 'update.available': return 'Update verfügbar'; - case 'update.versionAvailable': return ({required Object version}) => 'Version ${version} ist verfügbar'; - case 'update.currentVersion': return ({required Object version}) => 'Aktuell: ${version}'; - case 'update.skipVersion': return 'Diese Version überspringen'; - case 'update.viewRelease': return 'Release anzeigen'; - case 'update.latestVersion': return 'Aktuellste Version installiert'; - case 'update.checkFailed': return 'Fehler bei der Updateprüfung'; - case 'settings.title': return 'Einstellungen'; - case 'settings.language': return 'Sprache'; - case 'settings.theme': return 'Design'; - case 'settings.appearance': return 'Darstellung'; - case 'settings.videoPlayback': return 'Videowiedergabe'; - case 'settings.shufflePlay': return 'Zufallswiedergabe'; - case 'settings.advanced': return 'Erweitert'; - case 'settings.useSeasonPostersDescription': return 'Staffelposter statt Serienposter für Episoden anzeigen'; - case 'settings.showHeroSectionDescription': return 'Bereich mit empfohlenen Inhalten auf der Startseite anzeigen'; - case 'settings.secondsLabel': return 'Sekunden'; - case 'settings.minutesLabel': return 'Minuten'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => 'Dauer eingeben (${min}-${max})'; - case 'settings.systemTheme': return 'System'; - case 'settings.systemThemeDescription': return 'Systemeinstellungen folgen'; - case 'settings.lightTheme': return 'Hell'; - case 'settings.darkTheme': return 'Dunkel'; - case 'settings.libraryDensity': return 'Mediathekdichte'; - case 'settings.compact': return 'Kompakt'; - case 'settings.compactDescription': return 'Kleinere Karten, mehr Elemente sichtbar'; - case 'settings.normal': return 'Normal'; - case 'settings.normalDescription': return 'Standardgröße'; - case 'settings.comfortable': return 'Großzügig'; - case 'settings.comfortableDescription': return 'Größere Karten, weniger Elemente sichtbar'; - case 'settings.viewMode': return 'Ansichtsmodus'; - case 'settings.gridView': return 'Raster'; - case 'settings.gridViewDescription': return 'Elemente im Raster anzeigen'; - case 'settings.listView': return 'Liste'; - case 'settings.listViewDescription': return 'Elemente in Listenansicht anzeigen'; - case 'settings.useSeasonPosters': return 'Staffelposter verwenden'; - case 'settings.showHeroSection': return 'Hero-Bereich anzeigen'; - case 'settings.hardwareDecoding': return 'Hardware-Decodierung'; - case 'settings.hardwareDecodingDescription': return 'Hardwarebeschleunigung verwenden, sofern verfügbar'; - case 'settings.bufferSize': return 'Puffergröße'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return 'Untertitel-Stil'; - case 'settings.subtitleStylingDescription': return 'Aussehen von Untertiteln anpassen'; - case 'settings.smallSkipDuration': return 'Kleine Sprungdauer'; - case 'settings.largeSkipDuration': return 'Große Sprungdauer'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} Sekunden'; - case 'settings.defaultSleepTimer': return 'Standard-Sleep-Timer'; - case 'settings.minutesUnit': return ({required Object minutes}) => '${minutes} Minuten'; - case 'settings.rememberTrackSelections': return 'Spurauswahl pro Serie/Film merken'; - case 'settings.rememberTrackSelectionsDescription': return 'Audio- und Untertitelsprache automatisch speichern, wenn während der Wiedergabe geändert'; - case 'settings.unwatchedOnly': return 'Nur ungesehene'; - case 'settings.unwatchedOnlyDescription': return 'Nur ungesehene Episoden in die Shuffle-Warteschlange aufnehmen'; - case 'settings.shuffleOrderNavigation': return 'Navigation der Shuffle-Reihenfolge'; - case 'settings.shuffleOrderNavigationDescription': return 'Weiter/Zurück folgt der zufälligen Reihenfolge'; - case 'settings.loopShuffleQueue': return 'Shuffle-Warteschlange wiederholen'; - case 'settings.loopShuffleQueueDescription': return 'Warteschlange neu starten, wenn das Ende erreicht ist'; - case 'settings.videoPlayerControls': return 'Videoplayer-Steuerung'; - case 'settings.keyboardShortcuts': return 'Tastenkürzel'; - case 'settings.keyboardShortcutsDescription': return 'Tastenkürzel anpassen'; - case 'settings.debugLogging': return 'Debug-Protokollierung'; - case 'settings.debugLoggingDescription': return 'Detaillierte Protokolle zur Fehleranalyse aktivieren'; - case 'settings.viewLogs': return 'Protokolle anzeigen'; - case 'settings.viewLogsDescription': return 'App-Protokolle anzeigen'; - case 'settings.clearCache': return 'Cache löschen'; - case 'settings.clearCacheDescription': return 'Löscht alle zwischengespeicherten Bilder und Daten. Die App kann danach langsamer laden.'; - case 'settings.clearCacheSuccess': return 'Cache erfolgreich gelöscht'; - case 'settings.resetSettings': return 'Einstellungen zurücksetzen'; - case 'settings.resetSettingsDescription': return 'Alle Einstellungen auf Standard zurücksetzen. Dies kann nicht rückgängig gemacht werden.'; - case 'settings.resetSettingsSuccess': return 'Einstellungen erfolgreich zurückgesetzt'; - case 'settings.shortcutsReset': return 'Tastenkürzel auf Standard zurückgesetzt'; - case 'settings.about': return 'Über'; - case 'settings.aboutDescription': return 'App-Informationen und Lizenzen'; - case 'settings.updates': return 'Updates'; - case 'settings.updateAvailable': return 'Update verfügbar'; - case 'settings.checkForUpdates': return 'Nach Updates suchen'; - case 'settings.validationErrorEnterNumber': return 'Bitte eine gültige Zahl eingeben'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => 'Dauer muss zwischen ${min} und ${max} ${unit} liegen'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => 'Tastenkürzel bereits zugewiesen an ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => 'Tastenkürzel aktualisiert für ${action}'; - case 'settings.autoSkip': return 'Automatisches Überspringen'; - case 'settings.autoSkipIntro': return 'Intro automatisch überspringen'; - case 'settings.autoSkipIntroDescription': return 'Intro-Marker nach wenigen Sekunden automatisch überspringen'; - case 'settings.autoSkipCredits': return 'Abspann automatisch überspringen'; - case 'settings.autoSkipCreditsDescription': return 'Abspann automatisch überspringen und nächste Episode abspielen'; - case 'settings.autoSkipDelay': return 'Verzögerung für automatisches Überspringen'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => '${seconds} Sekunden vor dem automatischen Überspringen warten'; - case 'search.hint': return 'Filme, Serien, Musik suchen...'; - case 'search.tryDifferentTerm': return 'Anderen Suchbegriff versuchen'; - case 'search.searchYourMedia': return 'In den eigenen Medien suchen'; - case 'search.enterTitleActorOrKeyword': return 'Titel, Schauspieler oder Stichwort eingeben'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => 'Tastenkürzel festlegen für ${actionName}'; - case 'hotkeys.clearShortcut': return 'Kürzel löschen'; - case 'pinEntry.enterPin': return 'PIN eingeben'; - case 'pinEntry.showPin': return 'PIN anzeigen'; - case 'pinEntry.hidePin': return 'PIN verbergen'; - case 'fileInfo.title': return 'Dateiinfo'; - case 'fileInfo.video': return 'Video'; - case 'fileInfo.audio': return 'Audio'; - case 'fileInfo.file': return 'Datei'; - case 'fileInfo.advanced': return 'Erweitert'; - case 'fileInfo.codec': return 'Codec'; - case 'fileInfo.resolution': return 'Auflösung'; - case 'fileInfo.bitrate': return 'Bitrate'; - case 'fileInfo.frameRate': return 'Bildrate'; - case 'fileInfo.aspectRatio': return 'Seitenverhältnis'; - case 'fileInfo.profile': return 'Profil'; - case 'fileInfo.bitDepth': return 'Farbtiefe'; - case 'fileInfo.colorSpace': return 'Farbraum'; - case 'fileInfo.colorRange': return 'Farbbereich'; - case 'fileInfo.colorPrimaries': return 'Primärfarben'; - case 'fileInfo.chromaSubsampling': return 'Chroma-Subsampling'; - case 'fileInfo.channels': return 'Kanäle'; - case 'fileInfo.path': return 'Pfad'; - case 'fileInfo.size': return 'Größe'; - case 'fileInfo.container': return 'Container'; - case 'fileInfo.duration': return 'Dauer'; - case 'fileInfo.optimizedForStreaming': return 'Für Streaming optimiert'; - case 'fileInfo.has64bitOffsets': return '64-Bit-Offsets'; - case 'mediaMenu.markAsWatched': return 'Als gesehen markieren'; - case 'mediaMenu.markAsUnwatched': return 'Als ungesehen markieren'; - case 'mediaMenu.removeFromContinueWatching': return 'Aus ‚Weiterschauen‘ entfernen'; - case 'mediaMenu.goToSeries': return 'Zur Serie'; - case 'mediaMenu.goToSeason': return 'Zur Staffel'; - case 'mediaMenu.shufflePlay': return 'Zufallswiedergabe'; - case 'mediaMenu.fileInfo': return 'Dateiinfo'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, Film'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, Serie'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return 'angesehen'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} Prozent angesehen'; - case 'accessibility.mediaCardUnwatched': return 'ungeschaut'; - case 'accessibility.tapToPlay': return 'Zum Abspielen tippen'; - case 'tooltips.shufflePlay': return 'Zufallswiedergabe'; - case 'tooltips.markAsWatched': return 'Als gesehen markieren'; - case 'tooltips.markAsUnwatched': return 'Als ungesehen markieren'; - case 'videoControls.audioLabel': return 'Audio'; - case 'videoControls.subtitlesLabel': return 'Untertitel'; - case 'videoControls.resetToZero': return 'Auf 0 ms zurücksetzen'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} spielt später'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} spielt früher'; - case 'videoControls.noOffset': return 'Kein Offset'; - case 'videoControls.letterbox': return 'Letterbox'; - case 'videoControls.fillScreen': return 'Bild füllen'; - case 'videoControls.stretch': return 'Strecken'; - case 'videoControls.lockRotation': return 'Rotation sperren'; - case 'videoControls.unlockRotation': return 'Rotation entsperren'; - case 'videoControls.sleepTimer': return 'Schlaftimer'; - case 'videoControls.timerActive': return 'Schlaftimer aktiv'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert'; - case 'videoControls.sleepTimerCompleted': return 'Schlaftimer abgelaufen – Wiedergabe pausiert'; - case 'videoControls.playButton': return 'Wiedergeben'; - case 'videoControls.pauseButton': return 'Pause'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => '${seconds} Sekunden zurück'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => '${seconds} Sekunden vor'; - case 'videoControls.previousButton': return 'Vorherige Episode'; - case 'videoControls.nextButton': return 'Nächste Episode'; - case 'videoControls.previousChapterButton': return 'Vorheriges Kapitel'; - case 'videoControls.nextChapterButton': return 'Nächstes Kapitel'; - case 'videoControls.volumeButton': return 'Lautstärke'; - case 'videoControls.muteButton': return 'Stumm schalten'; - case 'videoControls.unmuteButton': return 'Stummschaltung aufheben'; - case 'videoControls.settingsButton': return 'Videoeinstellungen'; - case 'videoControls.audioTrackButton': return 'Tonspuren'; - case 'videoControls.subtitlesButton': return 'Untertitel'; - case 'videoControls.chaptersButton': return 'Kapitel'; - case 'videoControls.versionsButton': return 'Videoversionen'; - case 'videoControls.aspectRatioButton': return 'Seitenverhältnis'; - case 'videoControls.fullscreenButton': return 'Vollbild aktivieren'; - case 'videoControls.exitFullscreenButton': return 'Vollbild verlassen'; - case 'videoControls.rotationLockButton': return 'Dreh­sperre'; - case 'videoControls.timelineSlider': return 'Video-Zeitleiste'; - case 'videoControls.volumeSlider': return 'Lautstärkepegel'; - case 'videoControls.backButton': return 'Zurück'; - case 'userStatus.admin': return 'Eigentümer'; - case 'userStatus.restricted': return 'Eingeschränkt'; - case 'userStatus.protected': return 'Geschützt'; - case 'userStatus.current': return 'AKTUELL'; - case 'messages.markedAsWatched': return 'Als gesehen markiert'; - case 'messages.markedAsUnwatched': return 'Als ungesehen markiert'; - case 'messages.removedFromContinueWatching': return 'Aus ‚Weiterschauen‘ entfernt'; - case 'messages.errorLoading': return ({required Object error}) => 'Fehler: ${error}'; - case 'messages.fileInfoNotAvailable': return 'Dateiinfo nicht verfügbar'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}'; - case 'messages.errorLoadingSeries': return 'Fehler beim Laden der Serie'; - case 'messages.errorLoadingSeason': return 'Fehler beim Laden der Staffel'; - case 'messages.musicNotSupported': return 'Musikwiedergabe wird noch nicht unterstützt'; - case 'messages.logsCleared': return 'Protokolle gelöscht'; - case 'messages.logsCopied': return 'Protokolle in Zwischenablage kopiert'; - case 'messages.noLogsAvailable': return 'Keine Protokolle verfügbar'; - case 'messages.libraryScanning': return ({required Object title}) => 'Scanne „${title}“...'; - case 'messages.libraryScanStarted': return ({required Object title}) => 'Mediathekscan gestartet für „${title}“'; - case 'messages.libraryScanFailed': return ({required Object error}) => 'Fehler beim Scannen der Mediathek: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => 'Metadaten werden aktualisiert für „${title}“...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => 'Metadaten-Aktualisierung gestartet für „${title}“'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}'; - case 'messages.noPlexToken': return 'Kein Plex-Token gefunden. Bitte erneut anmelden.'; - case 'messages.logoutConfirm': return 'Abmeldung wirklich durchführen?'; - case 'messages.noSeasonsFound': return 'Keine Staffeln gefunden'; - case 'messages.noEpisodesFound': return 'Keine Episoden in der ersten Staffel gefunden'; - case 'messages.noEpisodesFoundGeneral': return 'Keine Episoden gefunden'; - case 'messages.noResultsFound': return 'Keine Ergebnisse gefunden'; - case 'messages.sleepTimerSet': return ({required Object label}) => 'Sleep-Timer gesetzt auf ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => 'Profilwechsel zu ${displayName} fehlgeschlagen'; - case 'messages.noItemsAvailable': return 'Keine Elemente verfügbar'; - case 'messages.failedToCreatePlayQueue': return 'Wiedergabewarteschlange konnte nicht erstellt werden'; - case 'messages.failedToCreatePlayQueueNoItems': return 'Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => 'Wiedergabe für ${action} fehlgeschlagen: ${error}'; - case 'profile.noUsersAvailable': return 'Keine Benutzer verfügbar'; - case 'subtitlingStyling.stylingOptions': return 'Stiloptionen'; - case 'subtitlingStyling.fontSize': return 'Schriftgröße'; - case 'subtitlingStyling.textColor': return 'Textfarbe'; - case 'subtitlingStyling.borderSize': return 'Rahmengröße'; - case 'subtitlingStyling.borderColor': return 'Rahmenfarbe'; - case 'subtitlingStyling.backgroundOpacity': return 'Hintergrunddeckkraft'; - case 'subtitlingStyling.backgroundColor': return 'Hintergrundfarbe'; - case 'dialog.confirmAction': return 'Aktion bestätigen'; - case 'dialog.areYouSure': return 'Aktion wirklich ausführen?'; - case 'dialog.cancel': return 'Abbrechen'; - case 'dialog.playNow': return 'Jetzt abspielen'; - case 'discover.title': return 'Entdecken'; - case 'discover.switchProfile': return 'Profil wechseln'; - case 'discover.switchServer': return 'Server wechseln'; - case 'discover.logout': return 'Abmelden'; - case 'discover.noContentAvailable': return 'Kein Inhalt verfügbar'; - case 'discover.addMediaToLibraries': return 'Medien zur Mediathek hinzufügen'; - case 'discover.continueWatching': return 'Weiterschauen'; - case 'discover.recentlyAdded': return 'Kürzlich hinzugefügt'; - case 'discover.play': return 'Abspielen'; - case 'discover.resume': return 'Fortsetzen'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => 'S${season}, E${episode} - Abspielen'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => 'S${season}, E${episode} - Fortsetzen'; - case 'discover.pause': return 'Pause'; - case 'discover.overview': return 'Übersicht'; - case 'discover.cast': return 'Besetzung'; - case 'discover.seasons': return 'Staffeln'; - case 'discover.studio': return 'Studio'; - case 'discover.rating': return 'Altersfreigabe'; - case 'discover.watched': return 'Gesehen'; - case 'discover.episodeCount': return ({required Object count}) => '${count} Episoden'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '${watched} von ${total} gesehen'; - case 'discover.movie': return 'Film'; - case 'discover.tvShow': return 'Serie'; - case 'discover.minutesLeft': return ({required Object minutes}) => '${minutes} Min übrig'; - case 'errors.searchFailed': return ({required Object error}) => 'Suche fehlgeschlagen: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => 'Zeitüberschreitung beim Laden von ${context}'; - case 'errors.connectionFailed': return 'Verbindung zum Plex-Server fehlgeschlagen'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => 'Fehler beim Laden von ${context}: ${error}'; - case 'errors.noClientAvailable': return 'Kein Client verfügbar'; - case 'errors.authenticationFailed': return ({required Object error}) => 'Authentifizierung fehlgeschlagen: ${error}'; - case 'errors.couldNotLaunchUrl': return 'Auth-URL konnte nicht geöffnet werden'; - case 'errors.pleaseEnterToken': return 'Bitte Token eingeben'; - case 'errors.invalidToken': return 'Ungültiges Token'; - case 'errors.failedToVerifyToken': return ({required Object error}) => 'Token-Verifizierung fehlgeschlagen: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => 'Profilwechsel zu ${displayName} fehlgeschlagen'; - case 'errors.connectionFailedGeneric': return 'Verbindung fehlgeschlagen'; - case 'libraries.title': return 'Mediatheken'; - case 'libraries.scanLibraryFiles': return 'Mediatheksdateien scannen'; - case 'libraries.scanLibrary': return 'Mediathek scannen'; - case 'libraries.analyze': return 'Analysieren'; - case 'libraries.analyzeLibrary': return 'Mediathek analysieren'; - case 'libraries.refreshMetadata': return 'Metadaten aktualisieren'; - case 'libraries.emptyTrash': return 'Papierkorb leeren'; - case 'libraries.emptyingTrash': return ({required Object title}) => 'Papierkorb für „${title}“ wird geleert...'; - case 'libraries.trashEmptied': return ({required Object title}) => 'Papierkorb für „${title}“ geleert'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => 'Papierkorb konnte nicht geleert werden: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => 'Analysiere „${title}“...'; - case 'libraries.analysisStarted': return ({required Object title}) => 'Analyse gestartet für „${title}“'; - case 'libraries.failedToAnalyze': return ({required Object error}) => 'Analyse der Mediathek fehlgeschlagen: ${error}'; - case 'libraries.noLibrariesFound': return 'Keine Mediatheken gefunden'; - case 'libraries.thisLibraryIsEmpty': return 'Diese Mediathek ist leer'; - case 'libraries.all': return 'Alle'; - case 'libraries.clearAll': return 'Alle löschen'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => '„${title}“ wirklich scannen?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => '„${title}“ wirklich analysieren?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => 'Metadaten für „${title}“ wirklich aktualisieren?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => 'Papierkorb für „${title}“ wirklich leeren?'; - case 'libraries.manageLibraries': return 'Mediatheken verwalten'; - case 'libraries.sort': return 'Sortieren'; - case 'libraries.sortBy': return 'Sortieren nach'; - case 'libraries.filters': return 'Filter'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => 'Lade Mediathek... (${count} Elemente geladen)'; - case 'libraries.confirmActionMessage': return 'Aktion wirklich durchführen?'; - case 'libraries.showLibrary': return 'Mediathek anzeigen'; - case 'libraries.hideLibrary': return 'Mediathek ausblenden'; - case 'libraries.libraryOptions': return 'Mediatheksoptionen'; - case 'libraries.content': return 'Bibliotheksinhalt'; - case 'libraries.selectLibrary': return 'Bibliothek auswählen'; - case 'libraries.filtersWithCount': return ({required Object count}) => 'Filter (${count})'; - case 'libraries.noRecommendations': return 'Keine Empfehlungen verfügbar'; - case 'libraries.noCollections': return 'Keine Sammlungen in dieser Mediathek'; - case 'libraries.noFoldersFound': return 'Keine Ordner gefunden'; - case 'libraries.folders': return 'Ordner'; - case 'libraries.tabs.recommended': return 'Empfohlen'; - case 'libraries.tabs.browse': return 'Durchsuchen'; - case 'libraries.tabs.collections': return 'Sammlungen'; - case 'libraries.tabs.playlists': return 'Wiedergabelisten'; - case 'libraries.groupings.all': return 'Alle'; - case 'libraries.groupings.movies': return 'Filme'; - case 'libraries.groupings.shows': return 'Serien'; - case 'libraries.groupings.seasons': return 'Staffeln'; - case 'libraries.groupings.episodes': return 'Episoden'; - case 'libraries.groupings.folders': return 'Ordner'; - case 'about.title': return 'Über'; - case 'about.openSourceLicenses': return 'Open-Source-Lizenzen'; - case 'about.versionLabel': return ({required Object version}) => 'Version ${version}'; - case 'about.appDescription': return 'Ein schöner Plex-Client für Flutter'; - case 'about.viewLicensesDescription': return 'Lizenzen von Drittanbieter-Bibliotheken anzeigen'; - case 'serverSelection.connectingToServer': return 'Verbinde mit Server...'; - case 'serverSelection.connectingToAllServers': return 'Verbinde mit allen Servern...'; - case 'serverSelection.connectToAllServers': return 'Mit allen Servern verbinden'; - case 'serverSelection.allServerConnectionsFailed': return 'Verbindung zu allen Servern fehlgeschlagen. Bitte Netzwerk prüfen und erneut versuchen.'; - case 'serverSelection.foundServers': return ({required Object count}) => '${count} Server gefunden'; - case 'serverSelection.orSelectIndividually': return 'Oder einzelnen Server auswählen:'; - case 'serverSelection.serverDebugCopied': return 'Server-Debugdaten kopiert'; - case 'serverSelection.copyDebugData': return 'Debugdaten kopieren'; - case 'serverSelection.noServersFound': return 'Keine Server gefunden'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => 'Keine Server gefunden für ${username} (${email})'; - case 'serverSelection.malformedServerData': return ({required Object count}) => '${count} Server mit fehlerhaften Daten gefunden. Keine gültigen Server verfügbar.'; - case 'serverSelection.incompleteServerInfo': return 'Einige Serverinformationen sind unvollständig und wurden übersprungen. Plex.tv-Konto prüfen.'; - case 'serverSelection.incompleteConnectionInfo': return 'Serververbindungsinformationen unvollständig. Bitte erneut versuchen.'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => 'Serverinformationen fehlerhaft: ${message}'; - case 'serverSelection.networkConnectionFailed': return 'Netzwerkverbindung fehlgeschlagen. Internetverbindung prüfen und erneut versuchen.'; - case 'serverSelection.authenticationFailed': return 'Authentifizierung fehlgeschlagen. Bitte erneut anmelden.'; - case 'serverSelection.plexServiceUnavailable': return 'Plex-Dienst nicht verfügbar. Bitte später erneut versuchen.'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => 'Server konnten nicht geladen werden: ${error}'; - case 'hubDetail.title': return 'Titel'; - case 'hubDetail.releaseYear': return 'Erscheinungsjahr'; - case 'hubDetail.dateAdded': return 'Hinzugefügt am'; - case 'hubDetail.rating': return 'Bewertung'; - case 'hubDetail.noItemsFound': return 'Keine Elemente gefunden'; - case 'logs.title': return 'Protokolle'; - case 'logs.clearLogs': return 'Protokolle löschen'; - case 'logs.copyLogs': return 'Protokolle kopieren'; - case 'logs.exportLogs': return 'Protokolle exportieren'; - case 'logs.noLogsToShow': return 'Keine Protokolle zum Anzeigen'; - case 'logs.error': return 'Fehler:'; - case 'logs.stackTrace': return 'Stacktrace:'; - case 'licenses.relatedPackages': return 'Verwandte Pakete'; - case 'licenses.license': return 'Lizenz'; - case 'licenses.licenseNumber': return ({required Object number}) => 'Lizenz ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} Lizenzen'; - case 'navigation.home': return 'Start'; - case 'navigation.search': return 'Suche'; - case 'navigation.libraries': return 'Mediatheken'; - case 'navigation.settings': return 'Einstellungen'; - case 'playlists.title': return 'Wiedergabelisten'; - case 'playlists.noPlaylists': return 'Keine Wiedergabelisten gefunden'; - case 'playlists.create': return 'Wiedergabeliste erstellen'; - case 'playlists.newPlaylist': return 'Neue Wiedergabeliste'; - case 'playlists.playlistName': return 'Name der Wiedergabeliste'; - case 'playlists.enterPlaylistName': return 'Name der Wiedergabeliste eingeben'; - case 'playlists.edit': return 'Wiedergabeliste bearbeiten'; - case 'playlists.delete': return 'Wiedergabeliste löschen'; - case 'playlists.addTo': return 'Zur Wiedergabeliste hinzufügen'; - case 'playlists.addItems': return 'Elemente hinzufügen'; - case 'playlists.removeItem': return 'Aus Wiedergabeliste entfernen'; - case 'playlists.clearPlaylist': return 'Wiedergabeliste leeren'; - case 'playlists.playAll': return 'Alle abspielen'; - case 'playlists.shuffle': return 'Zufällig'; - case 'playlists.smartPlaylist': return 'Intelligente Wiedergabeliste'; - case 'playlists.regularPlaylist': return 'Normale Wiedergabeliste'; - case 'playlists.itemCount': return ({required Object count}) => '${count} Elemente'; - case 'playlists.oneItem': return '1 Element'; - case 'playlists.emptyPlaylist': return 'Diese Wiedergabeliste ist leer'; - case 'playlists.deleteConfirm': return 'Wiedergabeliste löschen?'; - case 'playlists.deleteMessage': return ({required Object name}) => 'Soll "${name}" wirklich gelöscht werden?'; - case 'playlists.created': return 'Wiedergabeliste erstellt'; - case 'playlists.updated': return 'Wiedergabeliste aktualisiert'; - case 'playlists.deleted': return 'Wiedergabeliste gelöscht'; - case 'playlists.itemAdded': return 'Zur Wiedergabeliste hinzugefügt'; - case 'playlists.itemRemoved': return 'Aus Wiedergabeliste entfernt'; - case 'playlists.selectPlaylist': return 'Wiedergabeliste auswählen'; - case 'playlists.createNewPlaylist': return 'Neue Wiedergabeliste erstellen'; - case 'playlists.errorCreating': return 'Wiedergabeliste konnte nicht erstellt werden'; - case 'playlists.errorDeleting': return 'Wiedergabeliste konnte nicht gelöscht werden'; - case 'playlists.errorLoading': return 'Wiedergabelisten konnten nicht geladen werden'; - case 'playlists.errorAdding': return 'Konnte nicht zur Wiedergabeliste hinzugefügt werden'; - case 'playlists.errorReordering': return 'Element der Wiedergabeliste konnte nicht neu geordnet werden'; - case 'playlists.errorRemoving': return 'Konnte nicht aus der Wiedergabeliste entfernt werden'; - case 'playlists.playlist': return 'Wiedergabeliste'; - case 'collections.title': return 'Sammlungen'; - case 'collections.collection': return 'Sammlung'; - case 'collections.empty': return 'Sammlung ist leer'; - case 'collections.noItems': return 'Keine Elemente in dieser Sammlung'; - case 'collections.unknownLibrarySection': return 'Löschen nicht möglich: Unbekannte Bibliothekssektion'; - case 'collections.deleteCollection': return 'Sammlung löschen'; - case 'collections.deleteConfirm': return ({required Object title}) => 'Sind Sie sicher, dass Sie "${title}" löschen möchten? Dies kann nicht rückgängig gemacht werden.'; - case 'collections.deleted': return 'Sammlung gelöscht'; - case 'collections.deleteFailed': return 'Sammlung konnte nicht gelöscht werden'; - case 'collections.deleteFailedWithError': return ({required Object error}) => 'Sammlung konnte nicht gelöscht werden: ${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => 'Sammlungselemente konnten nicht geladen werden: ${error}'; - case 'collections.addTo': return 'Zur Sammlung hinzufügen'; - case 'collections.selectCollection': return 'Sammlung auswählen'; - case 'collections.createNewCollection': return 'Neue Sammlung erstellen'; - case 'collections.collectionName': return 'Sammlungsname'; - case 'collections.enterCollectionName': return 'Sammlungsnamen eingeben'; - case 'collections.addedToCollection': return 'Zur Sammlung hinzugefügt'; - case 'collections.errorAddingToCollection': return 'Fehler beim Hinzufügen zur Sammlung'; - case 'collections.created': return 'Sammlung erstellt'; - case 'collections.removeFromCollection': return 'Aus Sammlung entfernen'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => '"${title}" aus dieser Sammlung entfernen?'; - case 'collections.removedFromCollection': return 'Aus Sammlung entfernt'; - case 'collections.removeFromCollectionFailed': return 'Entfernen aus Sammlung fehlgeschlagen'; - case 'collections.removeFromCollectionError': return ({required Object error}) => 'Fehler beim Entfernen aus der Sammlung: ${error}'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return 'Lädt...'; + case 'auth.signInWithPlex': + return 'Mit Plex anmelden'; + case 'auth.showQRCode': + return 'QR-Code anzeigen'; + case 'auth.cancel': + return 'Abbrechen'; + case 'auth.authenticate': + return 'Authentifizieren'; + case 'auth.retry': + return 'Erneut versuchen'; + case 'auth.debugEnterToken': + return 'Debug: Plex-Token eingeben'; + case 'auth.plexTokenLabel': + return 'Plex-Auth-Token'; + case 'auth.plexTokenHint': + return 'Plex.tv-Token eingeben'; + case 'auth.authenticationTimeout': + return 'Authentifizierung abgelaufen. Bitte erneut versuchen.'; + case 'auth.scanQRCodeInstruction': + return 'Diesen QR-Code mit einem bei Plex angemeldeten Gerät scannen, um zu authentifizieren.'; + case 'auth.waitingForAuth': + return 'Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.'; + case 'common.cancel': + return 'Abbrechen'; + case 'common.save': + return 'Speichern'; + case 'common.close': + return 'Schließen'; + case 'common.clear': + return 'Leeren'; + case 'common.reset': + return 'Zurücksetzen'; + case 'common.later': + return 'Später'; + case 'common.submit': + return 'Senden'; + case 'common.confirm': + return 'Bestätigen'; + case 'common.retry': + return 'Erneut versuchen'; + case 'common.playNow': + return 'Jetzt abspielen'; + case 'common.logout': + return 'Abmelden'; + case 'common.online': + return 'Online'; + case 'common.offline': + return 'Offline'; + case 'common.owned': + return 'Besitzer'; + case 'common.shared': + return 'Geteilt'; + case 'common.current': + return 'AKTUELL'; + case 'common.unknown': + return 'Unbekannt'; + case 'common.refresh': + return 'Aktualisieren'; + case 'common.yes': + return 'Ja'; + case 'common.no': + return 'Nein'; + case 'common.server': + return 'Server'; + case 'common.delete': + return 'Löschen'; + case 'common.shuffle': + return 'Zufall'; + case 'common.addTo': + return 'Hinzufügen zu...'; + case 'screens.licenses': + return 'Lizenzen'; + case 'screens.selectServer': + return 'Server auswählen'; + case 'screens.switchProfile': + return 'Profil wechseln'; + case 'screens.subtitleStyling': + return 'Untertitel-Stil'; + case 'screens.search': + return 'Suche'; + case 'screens.logs': + return 'Protokolle'; + case 'update.available': + return 'Update verfügbar'; + case 'update.versionAvailable': + return ({required Object version}) => + 'Version ${version} ist verfügbar'; + case 'update.currentVersion': + return ({required Object version}) => 'Aktuell: ${version}'; + case 'update.skipVersion': + return 'Diese Version überspringen'; + case 'update.viewRelease': + return 'Release anzeigen'; + case 'update.latestVersion': + return 'Aktuellste Version installiert'; + case 'update.checkFailed': + return 'Fehler bei der Updateprüfung'; + case 'settings.title': + return 'Einstellungen'; + case 'settings.language': + return 'Sprache'; + case 'settings.theme': + return 'Design'; + case 'settings.appearance': + return 'Darstellung'; + case 'settings.videoPlayback': + return 'Videowiedergabe'; + case 'settings.shufflePlay': + return 'Zufallswiedergabe'; + case 'settings.advanced': + return 'Erweitert'; + case 'settings.useSeasonPostersDescription': + return 'Staffelposter statt Serienposter für Episoden anzeigen'; + case 'settings.showHeroSectionDescription': + return 'Bereich mit empfohlenen Inhalten auf der Startseite anzeigen'; + case 'settings.secondsLabel': + return 'Sekunden'; + case 'settings.minutesLabel': + return 'Minuten'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + 'Dauer eingeben (${min}-${max})'; + case 'settings.systemTheme': + return 'System'; + case 'settings.systemThemeDescription': + return 'Systemeinstellungen folgen'; + case 'settings.lightTheme': + return 'Hell'; + case 'settings.darkTheme': + return 'Dunkel'; + case 'settings.libraryDensity': + return 'Mediathekdichte'; + case 'settings.compact': + return 'Kompakt'; + case 'settings.compactDescription': + return 'Kleinere Karten, mehr Elemente sichtbar'; + case 'settings.normal': + return 'Normal'; + case 'settings.normalDescription': + return 'Standardgröße'; + case 'settings.comfortable': + return 'Großzügig'; + case 'settings.comfortableDescription': + return 'Größere Karten, weniger Elemente sichtbar'; + case 'settings.viewMode': + return 'Ansichtsmodus'; + case 'settings.gridView': + return 'Raster'; + case 'settings.gridViewDescription': + return 'Elemente im Raster anzeigen'; + case 'settings.listView': + return 'Liste'; + case 'settings.listViewDescription': + return 'Elemente in Listenansicht anzeigen'; + case 'settings.useSeasonPosters': + return 'Staffelposter verwenden'; + case 'settings.showHeroSection': + return 'Hero-Bereich anzeigen'; + case 'settings.hardwareDecoding': + return 'Hardware-Decodierung'; + case 'settings.hardwareDecodingDescription': + return 'Hardwarebeschleunigung verwenden, sofern verfügbar'; + case 'settings.bufferSize': + return 'Puffergröße'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return 'Untertitel-Stil'; + case 'settings.subtitleStylingDescription': + return 'Aussehen von Untertiteln anpassen'; + case 'settings.smallSkipDuration': + return 'Kleine Sprungdauer'; + case 'settings.largeSkipDuration': + return 'Große Sprungdauer'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} Sekunden'; + case 'settings.defaultSleepTimer': + return 'Standard-Sleep-Timer'; + case 'settings.minutesUnit': + return ({required Object minutes}) => '${minutes} Minuten'; + case 'settings.rememberTrackSelections': + return 'Spurauswahl pro Serie/Film merken'; + case 'settings.rememberTrackSelectionsDescription': + return 'Audio- und Untertitelsprache automatisch speichern, wenn während der Wiedergabe geändert'; + case 'settings.unwatchedOnly': + return 'Nur ungesehene'; + case 'settings.unwatchedOnlyDescription': + return 'Nur ungesehene Episoden in die Shuffle-Warteschlange aufnehmen'; + case 'settings.shuffleOrderNavigation': + return 'Navigation der Shuffle-Reihenfolge'; + case 'settings.shuffleOrderNavigationDescription': + return 'Weiter/Zurück folgt der zufälligen Reihenfolge'; + case 'settings.loopShuffleQueue': + return 'Shuffle-Warteschlange wiederholen'; + case 'settings.loopShuffleQueueDescription': + return 'Warteschlange neu starten, wenn das Ende erreicht ist'; + case 'settings.videoPlayerControls': + return 'Videoplayer-Steuerung'; + case 'settings.keyboardShortcuts': + return 'Tastenkürzel'; + case 'settings.keyboardShortcutsDescription': + return 'Tastenkürzel anpassen'; + case 'settings.debugLogging': + return 'Debug-Protokollierung'; + case 'settings.debugLoggingDescription': + return 'Detaillierte Protokolle zur Fehleranalyse aktivieren'; + case 'settings.viewLogs': + return 'Protokolle anzeigen'; + case 'settings.viewLogsDescription': + return 'App-Protokolle anzeigen'; + case 'settings.clearCache': + return 'Cache löschen'; + case 'settings.clearCacheDescription': + return 'Löscht alle zwischengespeicherten Bilder und Daten. Die App kann danach langsamer laden.'; + case 'settings.clearCacheSuccess': + return 'Cache erfolgreich gelöscht'; + case 'settings.resetSettings': + return 'Einstellungen zurücksetzen'; + case 'settings.resetSettingsDescription': + return 'Alle Einstellungen auf Standard zurücksetzen. Dies kann nicht rückgängig gemacht werden.'; + case 'settings.resetSettingsSuccess': + return 'Einstellungen erfolgreich zurückgesetzt'; + case 'settings.shortcutsReset': + return 'Tastenkürzel auf Standard zurückgesetzt'; + case 'settings.about': + return 'Über'; + case 'settings.aboutDescription': + return 'App-Informationen und Lizenzen'; + case 'settings.updates': + return 'Updates'; + case 'settings.updateAvailable': + return 'Update verfügbar'; + case 'settings.checkForUpdates': + return 'Nach Updates suchen'; + case 'settings.validationErrorEnterNumber': + return 'Bitte eine gültige Zahl eingeben'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => 'Dauer muss zwischen ${min} und ${max} ${unit} liegen'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => + 'Tastenkürzel bereits zugewiesen an ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => + 'Tastenkürzel aktualisiert für ${action}'; + case 'settings.autoSkip': + return 'Automatisches Überspringen'; + case 'settings.autoSkipIntro': + return 'Intro automatisch überspringen'; + case 'settings.autoSkipIntroDescription': + return 'Intro-Marker nach wenigen Sekunden automatisch überspringen'; + case 'settings.autoSkipCredits': + return 'Abspann automatisch überspringen'; + case 'settings.autoSkipCreditsDescription': + return 'Abspann automatisch überspringen und nächste Episode abspielen'; + case 'settings.autoSkipDelay': + return 'Verzögerung für automatisches Überspringen'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => + '${seconds} Sekunden vor dem automatischen Überspringen warten'; + case 'settings.downloads': + return 'Downloads'; + case 'settings.downloadLocationDescription': + return 'Speicherort für heruntergeladene Inhalte wählen'; + case 'settings.downloadLocationDefault': + return 'Standard (App-Speicher)'; + case 'settings.downloadLocationCustom': + return 'Benutzerdefinierter Speicherort'; + case 'settings.selectFolder': + return 'Ordner auswählen'; + case 'settings.resetToDefault': + return 'Auf Standard zurücksetzen'; + case 'settings.currentPath': + return ({required Object path}) => 'Aktuell: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'Benutzerdefinierter Download-Speicherort wird auf iOS nicht unterstützt'; + case 'settings.downloadLocationChanged': + return 'Download-Speicherort geändert'; + case 'settings.downloadLocationReset': + return 'Download-Speicherort auf Standard zurückgesetzt'; + case 'settings.downloadLocationInvalid': + return 'Ausgewählter Ordner ist nicht beschreibbar'; + case 'settings.downloadLocationSelectError': + return 'Ordnerauswahl fehlgeschlagen'; + case 'settings.downloadOnWifiOnly': + return 'Nur über WLAN herunterladen'; + case 'settings.downloadOnWifiOnlyDescription': + return 'Downloads über mobile Daten verhindern'; + case 'settings.cellularDownloadBlocked': + return 'Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.'; + case 'search.hint': + return 'Filme, Serien, Musik suchen...'; + case 'search.tryDifferentTerm': + return 'Anderen Suchbegriff versuchen'; + case 'search.searchYourMedia': + return 'In den eigenen Medien suchen'; + case 'search.enterTitleActorOrKeyword': + return 'Titel, Schauspieler oder Stichwort eingeben'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => + 'Tastenkürzel festlegen für ${actionName}'; + case 'hotkeys.clearShortcut': + return 'Kürzel löschen'; + case 'pinEntry.enterPin': + return 'PIN eingeben'; + case 'pinEntry.showPin': + return 'PIN anzeigen'; + case 'pinEntry.hidePin': + return 'PIN verbergen'; + case 'fileInfo.title': + return 'Dateiinfo'; + case 'fileInfo.video': + return 'Video'; + case 'fileInfo.audio': + return 'Audio'; + case 'fileInfo.file': + return 'Datei'; + case 'fileInfo.advanced': + return 'Erweitert'; + case 'fileInfo.codec': + return 'Codec'; + case 'fileInfo.resolution': + return 'Auflösung'; + case 'fileInfo.bitrate': + return 'Bitrate'; + case 'fileInfo.frameRate': + return 'Bildrate'; + case 'fileInfo.aspectRatio': + return 'Seitenverhältnis'; + case 'fileInfo.profile': + return 'Profil'; + case 'fileInfo.bitDepth': + return 'Farbtiefe'; + case 'fileInfo.colorSpace': + return 'Farbraum'; + case 'fileInfo.colorRange': + return 'Farbbereich'; + case 'fileInfo.colorPrimaries': + return 'Primärfarben'; + case 'fileInfo.chromaSubsampling': + return 'Chroma-Subsampling'; + case 'fileInfo.channels': + return 'Kanäle'; + case 'fileInfo.path': + return 'Pfad'; + case 'fileInfo.size': + return 'Größe'; + case 'fileInfo.container': + return 'Container'; + case 'fileInfo.duration': + return 'Dauer'; + case 'fileInfo.optimizedForStreaming': + return 'Für Streaming optimiert'; + case 'fileInfo.has64bitOffsets': + return '64-Bit-Offsets'; + case 'mediaMenu.markAsWatched': + return 'Als gesehen markieren'; + case 'mediaMenu.markAsUnwatched': + return 'Als ungesehen markieren'; + case 'mediaMenu.removeFromContinueWatching': + return 'Aus ‚Weiterschauen‘ entfernen'; + case 'mediaMenu.goToSeries': + return 'Zur Serie'; + case 'mediaMenu.goToSeason': + return 'Zur Staffel'; + case 'mediaMenu.shufflePlay': + return 'Zufallswiedergabe'; + case 'mediaMenu.fileInfo': + return 'Dateiinfo'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, Film'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, Serie'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return 'angesehen'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '${percent} Prozent angesehen'; + case 'accessibility.mediaCardUnwatched': + return 'ungeschaut'; + case 'accessibility.tapToPlay': + return 'Zum Abspielen tippen'; + case 'tooltips.shufflePlay': + return 'Zufallswiedergabe'; + case 'tooltips.markAsWatched': + return 'Als gesehen markieren'; + case 'tooltips.markAsUnwatched': + return 'Als ungesehen markieren'; + case 'videoControls.audioLabel': + return 'Audio'; + case 'videoControls.subtitlesLabel': + return 'Untertitel'; + case 'videoControls.resetToZero': + return 'Auf 0 ms zurücksetzen'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} spielt später'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} spielt früher'; + case 'videoControls.noOffset': + return 'Kein Offset'; + case 'videoControls.letterbox': + return 'Letterbox'; + case 'videoControls.fillScreen': + return 'Bild füllen'; + case 'videoControls.stretch': + return 'Strecken'; + case 'videoControls.lockRotation': + return 'Rotation sperren'; + case 'videoControls.unlockRotation': + return 'Rotation entsperren'; + case 'videoControls.sleepTimer': + return 'Schlaftimer'; + case 'videoControls.timerActive': + return 'Schlaftimer aktiv'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => + 'Wiedergabe wird in ${duration} pausiert'; + case 'videoControls.sleepTimerCompleted': + return 'Schlaftimer abgelaufen – Wiedergabe pausiert'; + case 'videoControls.playButton': + return 'Wiedergeben'; + case 'videoControls.pauseButton': + return 'Pause'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => '${seconds} Sekunden zurück'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => '${seconds} Sekunden vor'; + case 'videoControls.previousButton': + return 'Vorherige Episode'; + case 'videoControls.nextButton': + return 'Nächste Episode'; + case 'videoControls.previousChapterButton': + return 'Vorheriges Kapitel'; + case 'videoControls.nextChapterButton': + return 'Nächstes Kapitel'; + case 'videoControls.volumeButton': + return 'Lautstärke'; + case 'videoControls.muteButton': + return 'Stumm schalten'; + case 'videoControls.unmuteButton': + return 'Stummschaltung aufheben'; + case 'videoControls.settingsButton': + return 'Videoeinstellungen'; + case 'videoControls.audioTrackButton': + return 'Tonspuren'; + case 'videoControls.subtitlesButton': + return 'Untertitel'; + case 'videoControls.chaptersButton': + return 'Kapitel'; + case 'videoControls.versionsButton': + return 'Videoversionen'; + case 'videoControls.aspectRatioButton': + return 'Seitenverhältnis'; + case 'videoControls.fullscreenButton': + return 'Vollbild aktivieren'; + case 'videoControls.exitFullscreenButton': + return 'Vollbild verlassen'; + case 'videoControls.rotationLockButton': + return 'Dreh­sperre'; + case 'videoControls.timelineSlider': + return 'Video-Zeitleiste'; + case 'videoControls.volumeSlider': + return 'Lautstärkepegel'; + case 'videoControls.backButton': + return 'Zurück'; + case 'userStatus.admin': + return 'Eigentümer'; + case 'userStatus.restricted': + return 'Eingeschränkt'; + case 'userStatus.protected': + return 'Geschützt'; + case 'userStatus.current': + return 'AKTUELL'; + case 'messages.markedAsWatched': + return 'Als gesehen markiert'; + case 'messages.markedAsUnwatched': + return 'Als ungesehen markiert'; + case 'messages.markedAsWatchedOffline': + return 'Als gesehen markiert (wird synchronisiert, wenn online)'; + case 'messages.markedAsUnwatchedOffline': + return 'Als ungesehen markiert (wird synchronisiert, wenn online)'; + case 'messages.removedFromContinueWatching': + return 'Aus ‚Weiterschauen\' entfernt'; + case 'messages.errorLoading': + return ({required Object error}) => 'Fehler: ${error}'; + case 'messages.fileInfoNotAvailable': + return 'Dateiinfo nicht verfügbar'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => + 'Fehler beim Laden der Dateiinfo: ${error}'; + case 'messages.errorLoadingSeries': + return 'Fehler beim Laden der Serie'; + case 'messages.errorLoadingSeason': + return 'Fehler beim Laden der Staffel'; + case 'messages.musicNotSupported': + return 'Musikwiedergabe wird noch nicht unterstützt'; + case 'messages.logsCleared': + return 'Protokolle gelöscht'; + case 'messages.logsCopied': + return 'Protokolle in Zwischenablage kopiert'; + case 'messages.noLogsAvailable': + return 'Keine Protokolle verfügbar'; + case 'messages.libraryScanning': + return ({required Object title}) => 'Scanne „${title}“...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => + 'Mediathekscan gestartet für „${title}“'; + case 'messages.libraryScanFailed': + return ({required Object error}) => + 'Fehler beim Scannen der Mediathek: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => + 'Metadaten werden aktualisiert für „${title}“...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => + 'Metadaten-Aktualisierung gestartet für „${title}“'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => + 'Metadaten konnten nicht aktualisiert werden: ${error}'; + case 'messages.noPlexToken': + return 'Kein Plex-Token gefunden. Bitte erneut anmelden.'; + case 'messages.logoutConfirm': + return 'Abmeldung wirklich durchführen?'; + case 'messages.noSeasonsFound': + return 'Keine Staffeln gefunden'; + case 'messages.noEpisodesFound': + return 'Keine Episoden in der ersten Staffel gefunden'; + case 'messages.noEpisodesFoundGeneral': + return 'Keine Episoden gefunden'; + case 'messages.noResultsFound': + return 'Keine Ergebnisse gefunden'; + case 'messages.sleepTimerSet': + return ({required Object label}) => 'Sleep-Timer gesetzt auf ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => + 'Profilwechsel zu ${displayName} fehlgeschlagen'; + case 'messages.noItemsAvailable': + return 'Keine Elemente verfügbar'; + case 'messages.failedToCreatePlayQueue': + return 'Wiedergabewarteschlange konnte nicht erstellt werden'; + case 'messages.failedToCreatePlayQueueNoItems': + return 'Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + 'Wiedergabe für ${action} fehlgeschlagen: ${error}'; + case 'messages.youAreOffline': + return 'Du bist offline'; + case 'messages.offlineFeatureUnavailable': + return 'Diese Funktion erfordert eine Internetverbindung. Deine Downloads und Einstellungen sind weiterhin verfügbar.'; + case 'profile.noUsersAvailable': + return 'Keine Benutzer verfügbar'; + case 'subtitlingStyling.stylingOptions': + return 'Stiloptionen'; + case 'subtitlingStyling.fontSize': + return 'Schriftgröße'; + case 'subtitlingStyling.textColor': + return 'Textfarbe'; + case 'subtitlingStyling.borderSize': + return 'Rahmengröße'; + case 'subtitlingStyling.borderColor': + return 'Rahmenfarbe'; + case 'subtitlingStyling.backgroundOpacity': + return 'Hintergrunddeckkraft'; + case 'subtitlingStyling.backgroundColor': + return 'Hintergrundfarbe'; + case 'dialog.confirmAction': + return 'Aktion bestätigen'; + case 'dialog.areYouSure': + return 'Aktion wirklich ausführen?'; + case 'dialog.cancel': + return 'Abbrechen'; + case 'dialog.playNow': + return 'Jetzt abspielen'; + case 'discover.title': + return 'Entdecken'; + case 'discover.switchProfile': + return 'Profil wechseln'; + case 'discover.switchServer': + return 'Server wechseln'; + case 'discover.logout': + return 'Abmelden'; + case 'discover.noContentAvailable': + return 'Kein Inhalt verfügbar'; + case 'discover.addMediaToLibraries': + return 'Medien zur Mediathek hinzufügen'; + case 'discover.continueWatching': + return 'Weiterschauen'; + case 'discover.recentlyAdded': + return 'Kürzlich hinzugefügt'; + case 'discover.play': + return 'Abspielen'; + case 'discover.resume': + return 'Fortsetzen'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return 'Pause'; + case 'discover.overview': + return 'Übersicht'; + case 'discover.cast': + return 'Besetzung'; + case 'discover.seasons': + return 'Staffeln'; + case 'discover.studio': + return 'Studio'; + case 'discover.rating': + return 'Altersfreigabe'; + case 'discover.watched': + return 'Gesehen'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} Episoden'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '${watched} von ${total} gesehen'; + case 'discover.movie': + return 'Film'; + case 'discover.tvShow': + return 'Serie'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '${minutes} Min übrig'; + case 'errors.searchFailed': + return ({required Object error}) => 'Suche fehlgeschlagen: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => + 'Zeitüberschreitung beim Laden von ${context}'; + case 'errors.connectionFailed': + return 'Verbindung zum Plex-Server fehlgeschlagen'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + 'Fehler beim Laden von ${context}: ${error}'; + case 'errors.noClientAvailable': + return 'Kein Client verfügbar'; + case 'errors.authenticationFailed': + return ({required Object error}) => + 'Authentifizierung fehlgeschlagen: ${error}'; + case 'errors.couldNotLaunchUrl': + return 'Auth-URL konnte nicht geöffnet werden'; + case 'errors.pleaseEnterToken': + return 'Bitte Token eingeben'; + case 'errors.invalidToken': + return 'Ungültiges Token'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => + 'Token-Verifizierung fehlgeschlagen: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => + 'Profilwechsel zu ${displayName} fehlgeschlagen'; + case 'errors.connectionFailedGeneric': + return 'Verbindung fehlgeschlagen'; + case 'libraries.title': + return 'Mediatheken'; + case 'libraries.scanLibraryFiles': + return 'Mediatheksdateien scannen'; + case 'libraries.scanLibrary': + return 'Mediathek scannen'; + case 'libraries.analyze': + return 'Analysieren'; + case 'libraries.analyzeLibrary': + return 'Mediathek analysieren'; + case 'libraries.refreshMetadata': + return 'Metadaten aktualisieren'; + case 'libraries.emptyTrash': + return 'Papierkorb leeren'; + case 'libraries.emptyingTrash': + return ({required Object title}) => + 'Papierkorb für „${title}“ wird geleert...'; + case 'libraries.trashEmptied': + return ({required Object title}) => 'Papierkorb für „${title}“ geleert'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => + 'Papierkorb konnte nicht geleert werden: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => 'Analysiere „${title}“...'; + case 'libraries.analysisStarted': + return ({required Object title}) => 'Analyse gestartet für „${title}“'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => + 'Analyse der Mediathek fehlgeschlagen: ${error}'; + case 'libraries.noLibrariesFound': + return 'Keine Mediatheken gefunden'; + case 'libraries.thisLibraryIsEmpty': + return 'Diese Mediathek ist leer'; + case 'libraries.all': + return 'Alle'; + case 'libraries.clearAll': + return 'Alle löschen'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => '„${title}“ wirklich scannen?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => '„${title}“ wirklich analysieren?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => + 'Metadaten für „${title}“ wirklich aktualisieren?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => + 'Papierkorb für „${title}“ wirklich leeren?'; + case 'libraries.manageLibraries': + return 'Mediatheken verwalten'; + case 'libraries.sort': + return 'Sortieren'; + case 'libraries.sortBy': + return 'Sortieren nach'; + case 'libraries.filters': + return 'Filter'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => + 'Lade Mediathek... (${count} Elemente geladen)'; + case 'libraries.confirmActionMessage': + return 'Aktion wirklich durchführen?'; + case 'libraries.showLibrary': + return 'Mediathek anzeigen'; + case 'libraries.hideLibrary': + return 'Mediathek ausblenden'; + case 'libraries.libraryOptions': + return 'Mediatheksoptionen'; + case 'libraries.content': + return 'Bibliotheksinhalt'; + case 'libraries.selectLibrary': + return 'Bibliothek auswählen'; + case 'libraries.filtersWithCount': + return ({required Object count}) => 'Filter (${count})'; + case 'libraries.noRecommendations': + return 'Keine Empfehlungen verfügbar'; + case 'libraries.noCollections': + return 'Keine Sammlungen in dieser Mediathek'; + case 'libraries.noFoldersFound': + return 'Keine Ordner gefunden'; + case 'libraries.folders': + return 'Ordner'; + case 'libraries.tabs.recommended': + return 'Empfohlen'; + case 'libraries.tabs.browse': + return 'Durchsuchen'; + case 'libraries.tabs.collections': + return 'Sammlungen'; + case 'libraries.tabs.playlists': + return 'Wiedergabelisten'; + case 'libraries.groupings.all': + return 'Alle'; + case 'libraries.groupings.movies': + return 'Filme'; + case 'libraries.groupings.shows': + return 'Serien'; + case 'libraries.groupings.seasons': + return 'Staffeln'; + case 'libraries.groupings.episodes': + return 'Episoden'; + case 'libraries.groupings.folders': + return 'Ordner'; + case 'about.title': + return 'Über'; + case 'about.openSourceLicenses': + return 'Open-Source-Lizenzen'; + case 'about.versionLabel': + return ({required Object version}) => 'Version ${version}'; + case 'about.appDescription': + return 'Ein schöner Plex-Client für Flutter'; + case 'about.viewLicensesDescription': + return 'Lizenzen von Drittanbieter-Bibliotheken anzeigen'; + case 'serverSelection.connectingToServer': + return 'Verbinde mit Server...'; + case 'serverSelection.connectingToAllServers': + return 'Verbinde mit allen Servern...'; + case 'serverSelection.connectToAllServers': + return 'Mit allen Servern verbinden'; + case 'serverSelection.allServerConnectionsFailed': + return 'Verbindung zu allen Servern fehlgeschlagen. Bitte Netzwerk prüfen und erneut versuchen.'; + case 'serverSelection.foundServers': + return ({required Object count}) => '${count} Server gefunden'; + case 'serverSelection.orSelectIndividually': + return 'Oder einzelnen Server auswählen:'; + case 'serverSelection.serverDebugCopied': + return 'Server-Debugdaten kopiert'; + case 'serverSelection.copyDebugData': + return 'Debugdaten kopieren'; + case 'serverSelection.noServersFound': + return 'Keine Server gefunden'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + 'Keine Server gefunden für ${username} (${email})'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + '${count} Server mit fehlerhaften Daten gefunden. Keine gültigen Server verfügbar.'; + case 'serverSelection.incompleteServerInfo': + return 'Einige Serverinformationen sind unvollständig und wurden übersprungen. Plex.tv-Konto prüfen.'; + case 'serverSelection.incompleteConnectionInfo': + return 'Serververbindungsinformationen unvollständig. Bitte erneut versuchen.'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => + 'Serverinformationen fehlerhaft: ${message}'; + case 'serverSelection.networkConnectionFailed': + return 'Netzwerkverbindung fehlgeschlagen. Internetverbindung prüfen und erneut versuchen.'; + case 'serverSelection.authenticationFailed': + return 'Authentifizierung fehlgeschlagen. Bitte erneut anmelden.'; + case 'serverSelection.plexServiceUnavailable': + return 'Plex-Dienst nicht verfügbar. Bitte später erneut versuchen.'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => + 'Server konnten nicht geladen werden: ${error}'; + case 'hubDetail.title': + return 'Titel'; + case 'hubDetail.releaseYear': + return 'Erscheinungsjahr'; + case 'hubDetail.dateAdded': + return 'Hinzugefügt am'; + case 'hubDetail.rating': + return 'Bewertung'; + case 'hubDetail.noItemsFound': + return 'Keine Elemente gefunden'; + case 'logs.title': + return 'Protokolle'; + case 'logs.clearLogs': + return 'Protokolle löschen'; + case 'logs.copyLogs': + return 'Protokolle kopieren'; + case 'logs.exportLogs': + return 'Protokolle exportieren'; + case 'logs.noLogsToShow': + return 'Keine Protokolle zum Anzeigen'; + case 'logs.error': + return 'Fehler:'; + case 'logs.stackTrace': + return 'Stacktrace:'; + case 'licenses.relatedPackages': + return 'Verwandte Pakete'; + case 'licenses.license': + return 'Lizenz'; + case 'licenses.licenseNumber': + return ({required Object number}) => 'Lizenz ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} Lizenzen'; + case 'navigation.home': + return 'Start'; + case 'navigation.search': + return 'Suche'; + case 'navigation.libraries': + return 'Mediatheken'; + case 'navigation.settings': + return 'Einstellungen'; + case 'navigation.downloads': + return 'Downloads'; + case 'downloads.title': + return 'Downloads'; + case 'downloads.manage': + return 'Verwalten'; + case 'downloads.tvShows': + return 'Serien'; + case 'downloads.movies': + return 'Filme'; + case 'downloads.queue': + return 'Warteschlange'; + case 'downloads.noDownloads': + return 'Noch keine Downloads'; + case 'downloads.noDownloadsDescription': + return 'Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt'; + case 'downloads.browseContent': + return 'Inhalte durchsuchen'; + case 'downloads.downloading': + return 'Wird heruntergeladen'; + case 'downloads.downloaded': + return 'Heruntergeladen'; + case 'downloads.paused': + return 'Pausiert'; + case 'downloads.failed': + return 'Fehlgeschlagen'; + case 'downloads.queued': + return 'In Warteschlange'; + case 'downloads.downloadNow': + return 'Herunterladen'; + case 'downloads.deleteDownload': + return 'Download löschen'; + case 'downloads.cancelDownload': + return 'Download abbrechen'; + case 'downloads.pauseDownload': + return 'Download pausieren'; + case 'downloads.resumeDownload': + return 'Download fortsetzen'; + case 'downloads.retryDownload': + return 'Download wiederholen'; + case 'downloads.downloadComplete': + return 'Download abgeschlossen'; + case 'downloads.downloadFailed': + return 'Download fehlgeschlagen'; + case 'downloads.downloadCancelled': + return 'Download abgebrochen'; + case 'downloads.downloadQueued': + return 'Download in Warteschlange'; + case 'downloads.episodesQueued': + return ({required Object count}) => + '${count} Episoden zum Download hinzugefügt'; + case 'downloads.downloadDeleted': + return 'Download gelöscht'; + case 'downloads.deleteConfirmTitle': + return 'Download löschen?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + 'Möchtest du "${title}" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.'; + case 'downloads.deleteConfirmMessage': + return 'Die heruntergeladene Datei wird von deinem Gerät entfernt.'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => 'Lösche ${title}... (${current} von ${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => 'Lösche ${title}...'; + case 'downloads.storageUsed': + return 'Speicherplatz belegt'; + case 'downloads.availableOffline': + return 'Offline verfügbar'; + case 'downloads.downloadingSeason': + return 'Staffel wird heruntergeladen'; + case 'downloads.downloadingEpisode': + return 'Episode wird heruntergeladen'; + case 'offline.youAreOffline': + return 'Du bist offline'; + case 'offline.offlineMode': + return 'Offline-Modus'; + case 'offline.backOnline': + return 'Wieder online'; + case 'offline.showingDownloadedContent': + return 'Nur heruntergeladene Inhalte werden angezeigt'; + case 'offline.noInternetConnection': + return 'Keine Internetverbindung'; + case 'offline.serversUnreachable': + return 'Plex-Server nicht erreichbar'; + case 'offline.connectToDownload': + return 'Verbinde dich mit dem Internet, um Inhalte herunterzuladen'; + case 'playlists.title': + return 'Wiedergabelisten'; + case 'playlists.noPlaylists': + return 'Keine Wiedergabelisten gefunden'; + case 'playlists.create': + return 'Wiedergabeliste erstellen'; + case 'playlists.newPlaylist': + return 'Neue Wiedergabeliste'; + case 'playlists.playlistName': + return 'Name der Wiedergabeliste'; + case 'playlists.enterPlaylistName': + return 'Name der Wiedergabeliste eingeben'; + case 'playlists.edit': + return 'Wiedergabeliste bearbeiten'; + case 'playlists.delete': + return 'Wiedergabeliste löschen'; + case 'playlists.addTo': + return 'Zur Wiedergabeliste hinzufügen'; + case 'playlists.addItems': + return 'Elemente hinzufügen'; + case 'playlists.removeItem': + return 'Aus Wiedergabeliste entfernen'; + case 'playlists.clearPlaylist': + return 'Wiedergabeliste leeren'; + case 'playlists.playAll': + return 'Alle abspielen'; + case 'playlists.shuffle': + return 'Zufällig'; + case 'playlists.smartPlaylist': + return 'Intelligente Wiedergabeliste'; + case 'playlists.regularPlaylist': + return 'Normale Wiedergabeliste'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} Elemente'; + case 'playlists.oneItem': + return '1 Element'; + case 'playlists.emptyPlaylist': + return 'Diese Wiedergabeliste ist leer'; + case 'playlists.deleteConfirm': + return 'Wiedergabeliste löschen?'; + case 'playlists.deleteMessage': + return ({required Object name}) => + 'Soll "${name}" wirklich gelöscht werden?'; + case 'playlists.created': + return 'Wiedergabeliste erstellt'; + case 'playlists.updated': + return 'Wiedergabeliste aktualisiert'; + case 'playlists.deleted': + return 'Wiedergabeliste gelöscht'; + case 'playlists.itemAdded': + return 'Zur Wiedergabeliste hinzugefügt'; + case 'playlists.itemRemoved': + return 'Aus Wiedergabeliste entfernt'; + case 'playlists.selectPlaylist': + return 'Wiedergabeliste auswählen'; + case 'playlists.createNewPlaylist': + return 'Neue Wiedergabeliste erstellen'; + case 'playlists.errorCreating': + return 'Wiedergabeliste konnte nicht erstellt werden'; + case 'playlists.errorDeleting': + return 'Wiedergabeliste konnte nicht gelöscht werden'; + case 'playlists.errorLoading': + return 'Wiedergabelisten konnten nicht geladen werden'; + case 'playlists.errorAdding': + return 'Konnte nicht zur Wiedergabeliste hinzugefügt werden'; + case 'playlists.errorReordering': + return 'Element der Wiedergabeliste konnte nicht neu geordnet werden'; + case 'playlists.errorRemoving': + return 'Konnte nicht aus der Wiedergabeliste entfernt werden'; + case 'playlists.playlist': + return 'Wiedergabeliste'; + case 'collections.title': + return 'Sammlungen'; + case 'collections.collection': + return 'Sammlung'; + case 'collections.empty': + return 'Sammlung ist leer'; + case 'collections.noItems': + return 'Keine Elemente in dieser Sammlung'; + case 'collections.unknownLibrarySection': + return 'Löschen nicht möglich: Unbekannte Bibliothekssektion'; + case 'collections.deleteCollection': + return 'Sammlung löschen'; + case 'collections.deleteConfirm': + return ({required Object title}) => + 'Sind Sie sicher, dass Sie "${title}" löschen möchten? Dies kann nicht rückgängig gemacht werden.'; + case 'collections.deleted': + return 'Sammlung gelöscht'; + case 'collections.deleteFailed': + return 'Sammlung konnte nicht gelöscht werden'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => + 'Sammlung konnte nicht gelöscht werden: ${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => + 'Sammlungselemente konnten nicht geladen werden: ${error}'; + case 'collections.addTo': + return 'Zur Sammlung hinzufügen'; + case 'collections.selectCollection': + return 'Sammlung auswählen'; + case 'collections.createNewCollection': + return 'Neue Sammlung erstellen'; + case 'collections.collectionName': + return 'Sammlungsname'; + case 'collections.enterCollectionName': + return 'Sammlungsnamen eingeben'; + case 'collections.addedToCollection': + return 'Zur Sammlung hinzugefügt'; + case 'collections.errorAddingToCollection': + return 'Fehler beim Hinzufügen zur Sammlung'; + case 'collections.created': + return 'Sammlung erstellt'; + case 'collections.removeFromCollection': + return 'Aus Sammlung entfernen'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => + '"${title}" aus dieser Sammlung entfernen?'; + case 'collections.removedFromCollection': + return 'Aus Sammlung entfernt'; + case 'collections.removeFromCollectionFailed': + return 'Entfernen aus Sammlung fehlgeschlagen'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => + 'Fehler beim Entfernen aus der Sammlung: ${error}'; + default: + return null; + } + } } extension on _StringsIt { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return 'Caricamento...'; - case 'auth.signInWithPlex': return 'Accedi con Plex'; - case 'auth.showQRCode': return 'Mostra QR Code'; - case 'auth.cancel': return 'Cancella'; - case 'auth.authenticate': return 'Autenticazione'; - case 'auth.retry': return 'Riprova'; - case 'auth.debugEnterToken': return 'Debug: Inserisci Token Plex'; - case 'auth.plexTokenLabel': return 'Token Auth Plex'; - case 'auth.plexTokenHint': return 'Inserisci il tuo token di Plex.tv'; - case 'auth.authenticationTimeout': return 'Autenticazione scaduta. Riprova.'; - case 'auth.scanQRCodeInstruction': return 'Scansiona questo QR code con un dispositivo connesso a Plex per autenticarti.'; - case 'auth.waitingForAuth': return 'In attesa di autenticazione...\nCompleta l\'accesso dal tuo browser.'; - case 'common.cancel': return 'Cancella'; - case 'common.save': return 'Salva'; - case 'common.close': return 'Chiudi'; - case 'common.clear': return 'Pulisci'; - case 'common.reset': return 'Ripristina'; - case 'common.later': return 'Più tardi'; - case 'common.submit': return 'Invia'; - case 'common.confirm': return 'Conferma'; - case 'common.retry': return 'Riprova'; - case 'common.playNow': return 'Riproduci ora'; - case 'common.logout': return 'Disconnetti'; - case 'common.online': return 'Online'; - case 'common.offline': return 'Offline'; - case 'common.owned': return 'Di proprietà'; - case 'common.shared': return 'Condiviso'; - case 'common.current': return 'CORRENTE'; - case 'common.unknown': return 'Sconosciuto'; - case 'common.refresh': return 'Aggiorna'; - case 'common.yes': return 'Sì'; - case 'common.no': return 'No'; - case 'common.server': return 'Server'; - case 'common.delete': return 'Elimina'; - case 'common.shuffle': return 'Casuale'; - case 'common.addTo': return 'Aggiungi a...'; - case 'screens.licenses': return 'Licenze'; - case 'screens.selectServer': return 'Seleziona server'; - case 'screens.switchProfile': return 'Cambia profilo'; - case 'screens.subtitleStyling': return 'Stile sottotitoli'; - case 'screens.search': return 'Cerca'; - case 'screens.logs': return 'Registro'; - case 'update.available': return 'Aggiornamento disponibile'; - case 'update.versionAvailable': return ({required Object version}) => 'Versione ${version} disponibile'; - case 'update.currentVersion': return ({required Object version}) => 'Corrente: ${version}'; - case 'update.skipVersion': return 'Salta questa versione'; - case 'update.viewRelease': return 'Visualizza dettagli release'; - case 'update.latestVersion': return 'La versione installata è l\'ultima disponibile'; - case 'update.checkFailed': return 'Impossibile controllare gli aggiornamenti'; - case 'settings.title': return 'Impostazioni'; - case 'settings.language': return 'Lingua'; - case 'settings.theme': return 'Tema'; - case 'settings.appearance': return 'Aspetto'; - case 'settings.videoPlayback': return 'Riproduzione video'; - case 'settings.shufflePlay': return 'Riproduzione casuale'; - case 'settings.advanced': return 'Avanzate'; - case 'settings.useSeasonPostersDescription': return 'Mostra il poster della stagione invece del poster della serie per gli episodi'; - case 'settings.showHeroSectionDescription': return 'Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale'; - case 'settings.secondsLabel': return 'Secondi'; - case 'settings.minutesLabel': return 'Minuti'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => 'Inserisci durata (${min}-${max})'; - case 'settings.systemTheme': return 'Sistema'; - case 'settings.systemThemeDescription': return 'Segui le impostazioni di sistema'; - case 'settings.lightTheme': return 'Chiaro'; - case 'settings.darkTheme': return 'Scuro'; - case 'settings.libraryDensity': return 'Densità libreria'; - case 'settings.compact': return 'Compatta'; - case 'settings.compactDescription': return 'Schede più piccole, più elementi visibili'; - case 'settings.normal': return 'Normale'; - case 'settings.normalDescription': return 'Dimensione predefinita'; - case 'settings.comfortable': return 'Comoda'; - case 'settings.comfortableDescription': return 'Schede più grandi, meno elementi visibili'; - case 'settings.viewMode': return 'Modalità di visualizzazione'; - case 'settings.gridView': return 'Griglia'; - case 'settings.gridViewDescription': return 'Visualizza gli elementi in un layout a griglia'; - case 'settings.listView': return 'Elenco'; - case 'settings.listViewDescription': return 'Visualizza gli elementi in un layout a elenco'; - case 'settings.useSeasonPosters': return 'Usa poster delle stagioni'; - case 'settings.showHeroSection': return 'Mostra sezione principale'; - case 'settings.hardwareDecoding': return 'Decodifica Hardware'; - case 'settings.hardwareDecodingDescription': return 'Utilizza l\'accelerazione hardware quando disponibile'; - case 'settings.bufferSize': return 'Dimensione buffer'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return 'Stile sottotitoli'; - case 'settings.subtitleStylingDescription': return 'Personalizza l\'aspetto dei sottotitoli'; - case 'settings.smallSkipDuration': return 'Durata skip breve'; - case 'settings.largeSkipDuration': return 'Durata skip lungo'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} secondi'; - case 'settings.defaultSleepTimer': return 'Timer spegnimento predefinito'; - case 'settings.minutesUnit': return ({required Object minutes}) => '${minutes} minuti'; - case 'settings.rememberTrackSelections': return 'Ricorda selezioni tracce per serie/film'; - case 'settings.rememberTrackSelectionsDescription': return 'Salva automaticamente le preferenze delle lingue audio e sottotitoli quando cambi tracce durante la riproduzione'; - case 'settings.unwatchedOnly': return 'Solo non guardati'; - case 'settings.unwatchedOnlyDescription': return 'Includi solo gli episodi non guardati nella coda di riproduzione casuale'; - case 'settings.shuffleOrderNavigation': return 'Navigazione in ordine casuale'; - case 'settings.shuffleOrderNavigationDescription': return 'I pulsanti Avanti/Indietro seguono l\'ordine casuale'; - case 'settings.loopShuffleQueue': return 'Coda di riproduzione casuale in loop'; - case 'settings.loopShuffleQueueDescription': return 'Riavvia la coda quando raggiungi la fine'; - case 'settings.videoPlayerControls': return 'Controlli del lettore video'; - case 'settings.keyboardShortcuts': return 'Scorciatoie da tastiera'; - case 'settings.keyboardShortcutsDescription': return 'Personalizza le scorciatoie da tastiera'; - case 'settings.debugLogging': return 'Log di debug'; - case 'settings.debugLoggingDescription': return 'Abilita il logging dettagliato per la risoluzione dei problemi'; - case 'settings.viewLogs': return 'Visualizza log'; - case 'settings.viewLogsDescription': return 'Visualizza i log dell\'applicazione'; - case 'settings.clearCache': return 'Svuota cache'; - case 'settings.clearCacheDescription': return 'Questa opzione cancellerà tutte le immagini e i dati memorizzati nella cache. Dopo aver cancellato la cache, l\'app potrebbe impiegare più tempo per caricare i contenuti.'; - case 'settings.clearCacheSuccess': return 'Cache cancellata correttamente'; - case 'settings.resetSettings': return 'Ripristina impostazioni'; - case 'settings.resetSettingsDescription': return 'Questa opzione ripristinerà tutte le impostazioni ai valori predefiniti. Non può essere annullata.'; - case 'settings.resetSettingsSuccess': return 'Impostazioni ripristinate correttamente'; - case 'settings.shortcutsReset': return 'Scorciatoie ripristinate alle impostazioni predefinite'; - case 'settings.about': return 'Informazioni'; - case 'settings.aboutDescription': return 'Informazioni sull\'app e le licenze'; - case 'settings.updates': return 'Aggiornamenti'; - case 'settings.updateAvailable': return 'Aggiornamento disponibile'; - case 'settings.checkForUpdates': return 'Controlla aggiornamenti'; - case 'settings.validationErrorEnterNumber': return 'Inserisci un numero valido'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => 'la durata deve essere compresa tra ${min} e ${max} ${unit}'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => 'Scorciatoia già assegnata a ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => 'Scorciatoia aggiornata per ${action}'; - case 'settings.autoSkip': return 'Salto Automatico'; - case 'settings.autoSkipIntro': return 'Salta Intro Automaticamente'; - case 'settings.autoSkipIntroDescription': return 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi'; - case 'settings.autoSkipCredits': return 'Salta Crediti Automaticamente'; - case 'settings.autoSkipCreditsDescription': return 'Salta automaticamente i crediti e riproduci l\'episodio successivo'; - case 'settings.autoSkipDelay': return 'Ritardo Salto Automatico'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => 'Aspetta ${seconds} secondi prima del salto automatico'; - case 'search.hint': return 'Cerca film. spettacoli, musica...'; - case 'search.tryDifferentTerm': return 'Prova altri termini di ricerca'; - case 'search.searchYourMedia': return 'Cerca nei tuoi media'; - case 'search.enterTitleActorOrKeyword': return 'Inserisci un titolo, attore o parola chiave'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => 'Imposta scorciatoia per ${actionName}'; - case 'hotkeys.clearShortcut': return 'Elimina scorciatoia'; - case 'pinEntry.enterPin': return 'Inserisci PIN'; - case 'pinEntry.showPin': return 'Mostra PIN'; - case 'pinEntry.hidePin': return 'Nascondi PIN'; - case 'fileInfo.title': return 'Info sul file'; - case 'fileInfo.video': return 'Video'; - case 'fileInfo.audio': return 'Audio'; - case 'fileInfo.file': return 'File'; - case 'fileInfo.advanced': return 'Avanzate'; - case 'fileInfo.codec': return 'Codec'; - case 'fileInfo.resolution': return 'Risoluzione'; - case 'fileInfo.bitrate': return 'Bitrate'; - case 'fileInfo.frameRate': return 'Frame Rate'; - case 'fileInfo.aspectRatio': return 'Aspect Ratio'; - case 'fileInfo.profile': return 'Profilo'; - case 'fileInfo.bitDepth': return 'Profondità colore'; - case 'fileInfo.colorSpace': return 'Spazio colore'; - case 'fileInfo.colorRange': return 'Gamma colori'; - case 'fileInfo.colorPrimaries': return 'Colori primari'; - case 'fileInfo.chromaSubsampling': return 'Sottocampionamento cromatico'; - case 'fileInfo.channels': return 'Canali'; - case 'fileInfo.path': return 'Percorso'; - case 'fileInfo.size': return 'Dimensione'; - case 'fileInfo.container': return 'Contenitore'; - case 'fileInfo.duration': return 'Durata'; - case 'fileInfo.optimizedForStreaming': return 'Ottimizzato per lo streaming'; - case 'fileInfo.has64bitOffsets': return 'Offset a 64-bit'; - case 'mediaMenu.markAsWatched': return 'Segna come visto'; - case 'mediaMenu.markAsUnwatched': return 'Segna come non visto'; - case 'mediaMenu.removeFromContinueWatching': return 'Rimuovi da Continua a guardare'; - case 'mediaMenu.goToSeries': return 'Vai alle serie'; - case 'mediaMenu.goToSeason': return 'Vai alla stagione'; - case 'mediaMenu.shufflePlay': return 'Riproduzione casuale'; - case 'mediaMenu.fileInfo': return 'Info sul file'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, film'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, serie TV'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return 'visto'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percento visto'; - case 'accessibility.mediaCardUnwatched': return 'non visto'; - case 'accessibility.tapToPlay': return 'Tocca per riprodurre'; - case 'tooltips.shufflePlay': return 'Riproduzione casuale'; - case 'tooltips.markAsWatched': return 'Segna come visto'; - case 'tooltips.markAsUnwatched': return 'Segna come non visto'; - case 'videoControls.audioLabel': return 'Audio'; - case 'videoControls.subtitlesLabel': return 'Sottotitoli'; - case 'videoControls.resetToZero': return 'Riporta a 0ms'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} riprodotto dopo'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} riprodotto prima'; - case 'videoControls.noOffset': return 'Nessun offset'; - case 'videoControls.letterbox': return 'Letterbox'; - case 'videoControls.fillScreen': return 'Riempi schermo'; - case 'videoControls.stretch': return 'Allunga'; - case 'videoControls.lockRotation': return 'Blocca rotazione'; - case 'videoControls.unlockRotation': return 'Sblocca rotazione'; - case 'videoControls.sleepTimer': return 'Timer di spegnimento'; - case 'videoControls.timerActive': return 'Timer attivo'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => 'La riproduzione si interromperà tra ${duration}'; - case 'videoControls.sleepTimerCompleted': return 'Timer di spegnimento completato - riproduzione in pausa'; - case 'videoControls.playButton': return 'Riproduci'; - case 'videoControls.pauseButton': return 'Pausa'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Riavvolgi di ${seconds} secondi'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Avanza di ${seconds} secondi'; - case 'videoControls.previousButton': return 'Episodio precedente'; - case 'videoControls.nextButton': return 'Episodio successivo'; - case 'videoControls.previousChapterButton': return 'Capitolo precedente'; - case 'videoControls.nextChapterButton': return 'Capitolo successivo'; - case 'videoControls.volumeButton': return 'Volume'; - case 'videoControls.muteButton': return 'Silenzia'; - case 'videoControls.unmuteButton': return 'Riattiva audio'; - case 'videoControls.settingsButton': return 'Impostazioni video'; - case 'videoControls.audioTrackButton': return 'Tracce audio'; - case 'videoControls.subtitlesButton': return 'Sottotitoli'; - case 'videoControls.chaptersButton': return 'Capitoli'; - case 'videoControls.versionsButton': return 'Versioni video'; - case 'videoControls.aspectRatioButton': return 'Proporzioni'; - case 'videoControls.fullscreenButton': return 'Attiva schermo intero'; - case 'videoControls.exitFullscreenButton': return 'Esci da schermo intero'; - case 'videoControls.rotationLockButton': return 'Blocco rotazione'; - case 'videoControls.timelineSlider': return 'Timeline video'; - case 'videoControls.volumeSlider': return 'Livello volume'; - case 'videoControls.backButton': return 'Indietro'; - case 'userStatus.admin': return 'Admin'; - case 'userStatus.restricted': return 'Limitato'; - case 'userStatus.protected': return 'Protetto'; - case 'userStatus.current': return 'ATTUALE'; - case 'messages.markedAsWatched': return 'Segna come visto'; - case 'messages.markedAsUnwatched': return 'Segna come non visto'; - case 'messages.removedFromContinueWatching': return 'Rimosso da Continua a guardare'; - case 'messages.errorLoading': return ({required Object error}) => 'Errore: ${error}'; - case 'messages.fileInfoNotAvailable': return 'Informazioni sul file non disponibili'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Errore caricamento informazioni sul file: ${error}'; - case 'messages.errorLoadingSeries': return 'Errore caricamento serie'; - case 'messages.errorLoadingSeason': return 'Errore caricamento stagione'; - case 'messages.musicNotSupported': return 'La riproduzione musicale non è ancora supportata'; - case 'messages.logsCleared': return 'Log eliminati'; - case 'messages.logsCopied': return 'Log copiati negli appunti'; - case 'messages.noLogsAvailable': return 'Nessun log disponibile'; - case 'messages.libraryScanning': return ({required Object title}) => 'Scansione "${title}"...'; - case 'messages.libraryScanStarted': return ({required Object title}) => 'Scansione libreria iniziata per "${title}"'; - case 'messages.libraryScanFailed': return ({required Object error}) => 'Impossibile eseguire scansione della libreria: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => 'Aggiornamento metadati per "${title}"...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => 'Aggiornamento metadati per "${title}"'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => 'Errore aggiornamento metadati: ${error}'; - case 'messages.noPlexToken': return 'Nessun token Plex trovato. Riesegui l\'accesso.'; - case 'messages.logoutConfirm': return 'Sei sicuro di volerti disconnettere?'; - case 'messages.noSeasonsFound': return 'Nessuna stagione trovata'; - case 'messages.noEpisodesFound': return 'Nessun episodio trovato nella prima stagione'; - case 'messages.noEpisodesFoundGeneral': return 'Nessun episodio trovato'; - case 'messages.noResultsFound': return 'Nessun risultato'; - case 'messages.sleepTimerSet': return ({required Object label}) => 'Imposta timer spegnimento per ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => 'Impossibile passare a ${displayName}'; - case 'messages.noItemsAvailable': return 'Nessun elemento disponibile'; - case 'messages.failedToCreatePlayQueue': return 'Impossibile creare la coda di riproduzione'; - case 'messages.failedToCreatePlayQueueNoItems': return 'Impossibile creare la coda di riproduzione - nessun elemento'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => 'Impossibile ${action}: ${error}'; - case 'profile.noUsersAvailable': return 'Nessun utente disponibile'; - case 'subtitlingStyling.stylingOptions': return 'Opzioni stile'; - case 'subtitlingStyling.fontSize': return 'Dimensione'; - case 'subtitlingStyling.textColor': return 'Colore testo'; - case 'subtitlingStyling.borderSize': return 'Dimensione bordo'; - case 'subtitlingStyling.borderColor': return 'Colore bordo'; - case 'subtitlingStyling.backgroundOpacity': return 'Opacità sfondo'; - case 'subtitlingStyling.backgroundColor': return 'Colore sfondo'; - case 'dialog.confirmAction': return 'Conferma azione'; - case 'dialog.areYouSure': return 'Sei sicuro di voler eseguire questa azione?'; - case 'dialog.cancel': return 'Cancella'; - case 'dialog.playNow': return 'Riproduci ora'; - case 'discover.title': return 'Esplora'; - case 'discover.switchProfile': return 'Cambia profilo'; - case 'discover.switchServer': return 'Cambia server'; - case 'discover.logout': return 'Disconnetti'; - case 'discover.noContentAvailable': return 'Nessun contenuto disponibile'; - case 'discover.addMediaToLibraries': return 'Aggiungi alcuni file multimediali alle tue librerie'; - case 'discover.continueWatching': return 'Continua a guardare'; - case 'discover.recentlyAdded': return 'Aggiunti di recente'; - case 'discover.play': return 'Riproduci'; - case 'discover.resume': return 'Riprendi'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => 'Riproduci S${season}, E${episode}'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => 'Riprendi S${season}, E${episode}'; - case 'discover.pause': return 'Pausa'; - case 'discover.overview': return 'Panoramica'; - case 'discover.cast': return 'Attori'; - case 'discover.seasons': return 'Stagioni'; - case 'discover.studio': return 'Studio'; - case 'discover.rating': return 'Classificazione'; - case 'discover.watched': return 'Guardato'; - case 'discover.episodeCount': return ({required Object count}) => '${count} episodi'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '${watched}/${total} guardati'; - case 'discover.movie': return 'Film'; - case 'discover.tvShow': return 'Serie TV'; - case 'discover.minutesLeft': return ({required Object minutes}) => '${minutes} minuti rimanenti'; - case 'errors.searchFailed': return ({required Object error}) => 'Ricerca fallita: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => 'Timeout connessione durante caricamento di ${context}'; - case 'errors.connectionFailed': return 'Impossibile connettersi al server Plex.'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => 'Impossibile caricare ${context}: ${error}'; - case 'errors.noClientAvailable': return 'Nessun client disponibile'; - case 'errors.authenticationFailed': return ({required Object error}) => 'Autenticazione fallita: ${error}'; - case 'errors.couldNotLaunchUrl': return 'Impossibile avviare URL di autenticazione'; - case 'errors.pleaseEnterToken': return 'Inserisci token'; - case 'errors.invalidToken': return 'Token non valido'; - case 'errors.failedToVerifyToken': return ({required Object error}) => 'Verifica token fallita: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => 'Impossibile passare a ${displayName}'; - case 'errors.connectionFailedGeneric': return 'Connessione fallita'; - case 'libraries.title': return 'Librerie'; - case 'libraries.scanLibraryFiles': return 'Scansiona file libreria'; - case 'libraries.scanLibrary': return 'Scansiona libreria'; - case 'libraries.analyze': return 'Analizza'; - case 'libraries.analyzeLibrary': return 'Analizza libreria'; - case 'libraries.refreshMetadata': return 'Aggiorna metadati'; - case 'libraries.emptyTrash': return 'Svuota cestino'; - case 'libraries.emptyingTrash': return ({required Object title}) => 'Svuotamento cestino per "${title}"...'; - case 'libraries.trashEmptied': return ({required Object title}) => 'Cestino svuotato per "${title}"'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => 'Impossibile svuotare cestino: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => 'Analisi "${title}"...'; - case 'libraries.analysisStarted': return ({required Object title}) => 'Analisi iniziata per "${title}"'; - case 'libraries.failedToAnalyze': return ({required Object error}) => 'Impossibile analizzare libreria: ${error}'; - case 'libraries.noLibrariesFound': return 'Nessuna libreria trovata'; - case 'libraries.thisLibraryIsEmpty': return 'Questa libreria è vuota'; - case 'libraries.all': return 'Tutto'; - case 'libraries.clearAll': return 'Cancella tutto'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => 'Sei sicuro di voler scansionare "${title}"?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => 'Sei sicuro di voler analizzare "${title}"?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => 'Sei sicuro di voler aggiornare i metadati per "${title}"?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => 'Sei sicuro di voler svuotare il cestino per "${title}"?'; - case 'libraries.manageLibraries': return 'Gestisci librerie'; - case 'libraries.sort': return 'Ordina'; - case 'libraries.sortBy': return 'Ordina per'; - case 'libraries.filters': return 'Filtri'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => 'Caricamento librerie... (${count} oggetti caricati)'; - case 'libraries.confirmActionMessage': return 'Sei sicuro di voler eseguire questa azione?'; - case 'libraries.showLibrary': return 'Mostra libreria'; - case 'libraries.hideLibrary': return 'Nascondi libreria'; - case 'libraries.libraryOptions': return 'Opzioni libreria'; - case 'libraries.content': return 'contenuto della libreria'; - case 'libraries.selectLibrary': return 'Seleziona libreria'; - case 'libraries.filtersWithCount': return ({required Object count}) => 'Filtri (${count})'; - case 'libraries.noRecommendations': return 'Nessun consiglio disponibile'; - case 'libraries.noCollections': return 'Nessuna raccolta in questa libreria'; - case 'libraries.noFoldersFound': return 'Nessuna cartella trovata'; - case 'libraries.folders': return 'cartelle'; - case 'libraries.tabs.recommended': return 'Consigliati'; - case 'libraries.tabs.browse': return 'Esplora'; - case 'libraries.tabs.collections': return 'Raccolte'; - case 'libraries.tabs.playlists': return 'Playlist'; - case 'libraries.groupings.all': return 'Tutti'; - case 'libraries.groupings.movies': return 'Film'; - case 'libraries.groupings.shows': return 'Serie TV'; - case 'libraries.groupings.seasons': return 'Stagioni'; - case 'libraries.groupings.episodes': return 'Episodi'; - case 'libraries.groupings.folders': return 'Cartelle'; - case 'about.title': return 'Informazioni'; - case 'about.openSourceLicenses': return 'Licenze Open Source'; - case 'about.versionLabel': return ({required Object version}) => 'Versione ${version}'; - case 'about.appDescription': return 'Un bellissimo client Plex per Flutter'; - case 'about.viewLicensesDescription': return 'Visualizza le licenze delle librerie di terze parti'; - case 'serverSelection.connectingToServer': return 'Connessione al server...'; - case 'serverSelection.connectingToAllServers': return 'Connessione a tutti i server...'; - case 'serverSelection.connectToAllServers': return 'Connetti a tutti i server'; - case 'serverSelection.allServerConnectionsFailed': return 'Impossibile connettersi a nessun server. Controlla la tua rete e riprova.'; - case 'serverSelection.foundServers': return ({required Object count}) => 'Trovati ${count} server'; - case 'serverSelection.orSelectIndividually': return 'Oppure seleziona un server singolarmente:'; - case 'serverSelection.serverDebugCopied': return 'Dati di debug del server copiati negli appunti'; - case 'serverSelection.copyDebugData': return 'Copia dati di debug'; - case 'serverSelection.noServersFound': return 'Nessun server trovato'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => 'Nessun server trovato per ${username} (${email})'; - case 'serverSelection.malformedServerData': return ({required Object count}) => 'Trovato ${count} server con dati difettosi. Nessun server valido disponibile.'; - case 'serverSelection.incompleteServerInfo': return 'Alcuni server presentano informazioni incomplete e sono stati ignorati. Controlla il tuo account Plex.tv.'; - case 'serverSelection.incompleteConnectionInfo': return 'Le informazioni di connessione al server sono incomplete. Riprova.'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => 'Le informazioni sul server sono errate: ${message}'; - case 'serverSelection.networkConnectionFailed': return 'Connessione di rete non riuscita. Controlla la tua connessione Internet e riprova.'; - case 'serverSelection.authenticationFailed': return 'Autenticazione fallita. Effettua nuovamente l\'accesso.'; - case 'serverSelection.plexServiceUnavailable': return 'Servizio Plex non disponibile. Riprova più tardi.'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => 'Impossibile caricare i server: ${error}'; - case 'hubDetail.title': return 'Titolo'; - case 'hubDetail.releaseYear': return 'Anno rilascio'; - case 'hubDetail.dateAdded': return 'Data aggiunta'; - case 'hubDetail.rating': return 'Valutazione'; - case 'hubDetail.noItemsFound': return 'Nessun elemento trovato'; - case 'logs.title': return 'Registro'; - case 'logs.clearLogs': return 'Cancella log'; - case 'logs.copyLogs': return 'Copia log'; - case 'logs.exportLogs': return 'Esporta log'; - case 'logs.noLogsToShow': return 'Nessun log da mostrare'; - case 'logs.error': return 'Errore:'; - case 'logs.stackTrace': return 'Traccia dello stack:'; - case 'licenses.relatedPackages': return 'Pacchetti correlati'; - case 'licenses.license': return 'Licenza'; - case 'licenses.licenseNumber': return ({required Object number}) => 'Licenza ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} licenze'; - case 'navigation.home': return 'Home'; - case 'navigation.search': return 'Cerca'; - case 'navigation.libraries': return 'Librerie'; - case 'navigation.settings': return 'Impostazioni'; - case 'playlists.title': return 'Playlist'; - case 'playlists.noPlaylists': return 'Nessuna playlist trovata'; - case 'playlists.create': return 'Crea playlist'; - case 'playlists.newPlaylist': return 'Nuova playlist'; - case 'playlists.playlistName': return 'Nome playlist'; - case 'playlists.enterPlaylistName': return 'Inserisci nome playlist'; - case 'playlists.edit': return 'Modifica playlist'; - case 'playlists.delete': return 'Elimina playlist'; - case 'playlists.addTo': return 'Aggiungi a playlist'; - case 'playlists.addItems': return 'Aggiungi elementi'; - case 'playlists.removeItem': return 'Rimuovi da playlist'; - case 'playlists.clearPlaylist': return 'Svuota playlist'; - case 'playlists.playAll': return 'Riproduci tutto'; - case 'playlists.shuffle': return 'Casuale'; - case 'playlists.smartPlaylist': return 'Playlist intelligente'; - case 'playlists.regularPlaylist': return 'Playlist normale'; - case 'playlists.itemCount': return ({required Object count}) => '${count} elementi'; - case 'playlists.oneItem': return '1 elemento'; - case 'playlists.emptyPlaylist': return 'Questa playlist è vuota'; - case 'playlists.deleteConfirm': return 'Eliminare playlist?'; - case 'playlists.deleteMessage': return ({required Object name}) => 'Sei sicuro di voler eliminare "${name}"?'; - case 'playlists.created': return 'Playlist creata'; - case 'playlists.updated': return 'Playlist aggiornata'; - case 'playlists.deleted': return 'Playlist eliminata'; - case 'playlists.itemAdded': return 'Aggiunto alla playlist'; - case 'playlists.itemRemoved': return 'Rimosso dalla playlist'; - case 'playlists.selectPlaylist': return 'Seleziona playlist'; - case 'playlists.createNewPlaylist': return 'Crea nuova playlist'; - case 'playlists.errorCreating': return 'Errore durante la creazione della playlist'; - case 'playlists.errorDeleting': return 'Errore durante l\'eliminazione della playlist'; - case 'playlists.errorLoading': return 'Errore durante il caricamento delle playlist'; - case 'playlists.errorAdding': return 'Errore durante l\'aggiunta alla playlist'; - case 'playlists.errorReordering': return 'Errore durante il riordino dell\'elemento della playlist'; - case 'playlists.errorRemoving': return 'Errore durante la rimozione dalla playlist'; - case 'playlists.playlist': return 'Playlist'; - case 'collections.title': return 'Raccolte'; - case 'collections.collection': return 'Raccolta'; - case 'collections.empty': return 'La raccolta è vuota'; - case 'collections.noItems': return 'Nessun elemento in questa raccolta'; - case 'collections.unknownLibrarySection': return 'Impossibile eliminare: sezione libreria sconosciuta'; - case 'collections.deleteCollection': return 'Elimina raccolta'; - case 'collections.deleteConfirm': return ({required Object title}) => 'Sei sicuro di voler eliminare "${title}"? Questa azione non può essere annullata.'; - case 'collections.deleted': return 'Raccolta eliminata'; - case 'collections.deleteFailed': return 'Impossibile eliminare la raccolta'; - case 'collections.deleteFailedWithError': return ({required Object error}) => 'Impossibile eliminare la raccolta: ${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => 'Impossibile caricare gli elementi della raccolta: ${error}'; - case 'collections.addTo': return 'Aggiungi alla raccolta'; - case 'collections.selectCollection': return 'Seleziona raccolta'; - case 'collections.createNewCollection': return 'Crea nuova raccolta'; - case 'collections.collectionName': return 'Nome raccolta'; - case 'collections.enterCollectionName': return 'Inserisci nome raccolta'; - case 'collections.addedToCollection': return 'Aggiunto alla raccolta'; - case 'collections.errorAddingToCollection': return 'Errore nell\'aggiunta alla raccolta'; - case 'collections.created': return 'Raccolta creata'; - case 'collections.removeFromCollection': return 'Rimuovi dalla raccolta'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => 'Rimuovere "${title}" da questa raccolta?'; - case 'collections.removedFromCollection': return 'Rimosso dalla raccolta'; - case 'collections.removeFromCollectionFailed': return 'Impossibile rimuovere dalla raccolta'; - case 'collections.removeFromCollectionError': return ({required Object error}) => 'Errore durante la rimozione dalla raccolta: ${error}'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return 'Caricamento...'; + case 'auth.signInWithPlex': + return 'Accedi con Plex'; + case 'auth.showQRCode': + return 'Mostra QR Code'; + case 'auth.cancel': + return 'Cancella'; + case 'auth.authenticate': + return 'Autenticazione'; + case 'auth.retry': + return 'Riprova'; + case 'auth.debugEnterToken': + return 'Debug: Inserisci Token Plex'; + case 'auth.plexTokenLabel': + return 'Token Auth Plex'; + case 'auth.plexTokenHint': + return 'Inserisci il tuo token di Plex.tv'; + case 'auth.authenticationTimeout': + return 'Autenticazione scaduta. Riprova.'; + case 'auth.scanQRCodeInstruction': + return 'Scansiona questo QR code con un dispositivo connesso a Plex per autenticarti.'; + case 'auth.waitingForAuth': + return 'In attesa di autenticazione...\nCompleta l\'accesso dal tuo browser.'; + case 'common.cancel': + return 'Cancella'; + case 'common.save': + return 'Salva'; + case 'common.close': + return 'Chiudi'; + case 'common.clear': + return 'Pulisci'; + case 'common.reset': + return 'Ripristina'; + case 'common.later': + return 'Più tardi'; + case 'common.submit': + return 'Invia'; + case 'common.confirm': + return 'Conferma'; + case 'common.retry': + return 'Riprova'; + case 'common.playNow': + return 'Riproduci ora'; + case 'common.logout': + return 'Disconnetti'; + case 'common.online': + return 'Online'; + case 'common.offline': + return 'Offline'; + case 'common.owned': + return 'Di proprietà'; + case 'common.shared': + return 'Condiviso'; + case 'common.current': + return 'CORRENTE'; + case 'common.unknown': + return 'Sconosciuto'; + case 'common.refresh': + return 'Aggiorna'; + case 'common.yes': + return 'Sì'; + case 'common.no': + return 'No'; + case 'common.server': + return 'Server'; + case 'common.delete': + return 'Elimina'; + case 'common.shuffle': + return 'Casuale'; + case 'common.addTo': + return 'Aggiungi a...'; + case 'screens.licenses': + return 'Licenze'; + case 'screens.selectServer': + return 'Seleziona server'; + case 'screens.switchProfile': + return 'Cambia profilo'; + case 'screens.subtitleStyling': + return 'Stile sottotitoli'; + case 'screens.search': + return 'Cerca'; + case 'screens.logs': + return 'Registro'; + case 'update.available': + return 'Aggiornamento disponibile'; + case 'update.versionAvailable': + return ({required Object version}) => 'Versione ${version} disponibile'; + case 'update.currentVersion': + return ({required Object version}) => 'Corrente: ${version}'; + case 'update.skipVersion': + return 'Salta questa versione'; + case 'update.viewRelease': + return 'Visualizza dettagli release'; + case 'update.latestVersion': + return 'La versione installata è l\'ultima disponibile'; + case 'update.checkFailed': + return 'Impossibile controllare gli aggiornamenti'; + case 'settings.title': + return 'Impostazioni'; + case 'settings.language': + return 'Lingua'; + case 'settings.theme': + return 'Tema'; + case 'settings.appearance': + return 'Aspetto'; + case 'settings.videoPlayback': + return 'Riproduzione video'; + case 'settings.shufflePlay': + return 'Riproduzione casuale'; + case 'settings.advanced': + return 'Avanzate'; + case 'settings.useSeasonPostersDescription': + return 'Mostra il poster della stagione invece del poster della serie per gli episodi'; + case 'settings.showHeroSectionDescription': + return 'Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale'; + case 'settings.secondsLabel': + return 'Secondi'; + case 'settings.minutesLabel': + return 'Minuti'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + 'Inserisci durata (${min}-${max})'; + case 'settings.systemTheme': + return 'Sistema'; + case 'settings.systemThemeDescription': + return 'Segui le impostazioni di sistema'; + case 'settings.lightTheme': + return 'Chiaro'; + case 'settings.darkTheme': + return 'Scuro'; + case 'settings.libraryDensity': + return 'Densità libreria'; + case 'settings.compact': + return 'Compatta'; + case 'settings.compactDescription': + return 'Schede più piccole, più elementi visibili'; + case 'settings.normal': + return 'Normale'; + case 'settings.normalDescription': + return 'Dimensione predefinita'; + case 'settings.comfortable': + return 'Comoda'; + case 'settings.comfortableDescription': + return 'Schede più grandi, meno elementi visibili'; + case 'settings.viewMode': + return 'Modalità di visualizzazione'; + case 'settings.gridView': + return 'Griglia'; + case 'settings.gridViewDescription': + return 'Visualizza gli elementi in un layout a griglia'; + case 'settings.listView': + return 'Elenco'; + case 'settings.listViewDescription': + return 'Visualizza gli elementi in un layout a elenco'; + case 'settings.useSeasonPosters': + return 'Usa poster delle stagioni'; + case 'settings.showHeroSection': + return 'Mostra sezione principale'; + case 'settings.hardwareDecoding': + return 'Decodifica Hardware'; + case 'settings.hardwareDecodingDescription': + return 'Utilizza l\'accelerazione hardware quando disponibile'; + case 'settings.bufferSize': + return 'Dimensione buffer'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return 'Stile sottotitoli'; + case 'settings.subtitleStylingDescription': + return 'Personalizza l\'aspetto dei sottotitoli'; + case 'settings.smallSkipDuration': + return 'Durata skip breve'; + case 'settings.largeSkipDuration': + return 'Durata skip lungo'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} secondi'; + case 'settings.defaultSleepTimer': + return 'Timer spegnimento predefinito'; + case 'settings.minutesUnit': + return ({required Object minutes}) => '${minutes} minuti'; + case 'settings.rememberTrackSelections': + return 'Ricorda selezioni tracce per serie/film'; + case 'settings.rememberTrackSelectionsDescription': + return 'Salva automaticamente le preferenze delle lingue audio e sottotitoli quando cambi tracce durante la riproduzione'; + case 'settings.unwatchedOnly': + return 'Solo non guardati'; + case 'settings.unwatchedOnlyDescription': + return 'Includi solo gli episodi non guardati nella coda di riproduzione casuale'; + case 'settings.shuffleOrderNavigation': + return 'Navigazione in ordine casuale'; + case 'settings.shuffleOrderNavigationDescription': + return 'I pulsanti Avanti/Indietro seguono l\'ordine casuale'; + case 'settings.loopShuffleQueue': + return 'Coda di riproduzione casuale in loop'; + case 'settings.loopShuffleQueueDescription': + return 'Riavvia la coda quando raggiungi la fine'; + case 'settings.videoPlayerControls': + return 'Controlli del lettore video'; + case 'settings.keyboardShortcuts': + return 'Scorciatoie da tastiera'; + case 'settings.keyboardShortcutsDescription': + return 'Personalizza le scorciatoie da tastiera'; + case 'settings.debugLogging': + return 'Log di debug'; + case 'settings.debugLoggingDescription': + return 'Abilita il logging dettagliato per la risoluzione dei problemi'; + case 'settings.viewLogs': + return 'Visualizza log'; + case 'settings.viewLogsDescription': + return 'Visualizza i log dell\'applicazione'; + case 'settings.clearCache': + return 'Svuota cache'; + case 'settings.clearCacheDescription': + return 'Questa opzione cancellerà tutte le immagini e i dati memorizzati nella cache. Dopo aver cancellato la cache, l\'app potrebbe impiegare più tempo per caricare i contenuti.'; + case 'settings.clearCacheSuccess': + return 'Cache cancellata correttamente'; + case 'settings.resetSettings': + return 'Ripristina impostazioni'; + case 'settings.resetSettingsDescription': + return 'Questa opzione ripristinerà tutte le impostazioni ai valori predefiniti. Non può essere annullata.'; + case 'settings.resetSettingsSuccess': + return 'Impostazioni ripristinate correttamente'; + case 'settings.shortcutsReset': + return 'Scorciatoie ripristinate alle impostazioni predefinite'; + case 'settings.about': + return 'Informazioni'; + case 'settings.aboutDescription': + return 'Informazioni sull\'app e le licenze'; + case 'settings.updates': + return 'Aggiornamenti'; + case 'settings.updateAvailable': + return 'Aggiornamento disponibile'; + case 'settings.checkForUpdates': + return 'Controlla aggiornamenti'; + case 'settings.validationErrorEnterNumber': + return 'Inserisci un numero valido'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => 'la durata deve essere compresa tra ${min} e ${max} ${unit}'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => + 'Scorciatoia già assegnata a ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => + 'Scorciatoia aggiornata per ${action}'; + case 'settings.autoSkip': + return 'Salto Automatico'; + case 'settings.autoSkipIntro': + return 'Salta Intro Automaticamente'; + case 'settings.autoSkipIntroDescription': + return 'Salta automaticamente i marcatori dell\'intro dopo alcuni secondi'; + case 'settings.autoSkipCredits': + return 'Salta Crediti Automaticamente'; + case 'settings.autoSkipCreditsDescription': + return 'Salta automaticamente i crediti e riproduci l\'episodio successivo'; + case 'settings.autoSkipDelay': + return 'Ritardo Salto Automatico'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => + 'Aspetta ${seconds} secondi prima del salto automatico'; + case 'settings.downloads': + return 'Download'; + case 'settings.downloadLocationDescription': + return 'Scegli dove salvare i contenuti scaricati'; + case 'settings.downloadLocationDefault': + return 'Predefinita (Archiviazione App)'; + case 'settings.downloadLocationCustom': + return 'Posizione Personalizzata'; + case 'settings.selectFolder': + return 'Seleziona Cartella'; + case 'settings.resetToDefault': + return 'Ripristina Predefinita'; + case 'settings.currentPath': + return ({required Object path}) => 'Corrente: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'La posizione di download personalizzata non è supportata su iOS'; + case 'settings.downloadLocationChanged': + return 'Posizione di download modificata'; + case 'settings.downloadLocationReset': + return 'Posizione di download ripristinata a predefinita'; + case 'settings.downloadLocationInvalid': + return 'La cartella selezionata non è scrivibile'; + case 'settings.downloadLocationSelectError': + return 'Impossibile selezionare la cartella'; + case 'settings.downloadOnWifiOnly': + return 'Scarica solo con WiFi'; + case 'settings.downloadOnWifiOnlyDescription': + return 'Impedisci i download quando si utilizza la rete dati cellulare'; + case 'settings.cellularDownloadBlocked': + return 'I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l\'impostazione.'; + case 'search.hint': + return 'Cerca film. spettacoli, musica...'; + case 'search.tryDifferentTerm': + return 'Prova altri termini di ricerca'; + case 'search.searchYourMedia': + return 'Cerca nei tuoi media'; + case 'search.enterTitleActorOrKeyword': + return 'Inserisci un titolo, attore o parola chiave'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => + 'Imposta scorciatoia per ${actionName}'; + case 'hotkeys.clearShortcut': + return 'Elimina scorciatoia'; + case 'pinEntry.enterPin': + return 'Inserisci PIN'; + case 'pinEntry.showPin': + return 'Mostra PIN'; + case 'pinEntry.hidePin': + return 'Nascondi PIN'; + case 'fileInfo.title': + return 'Info sul file'; + case 'fileInfo.video': + return 'Video'; + case 'fileInfo.audio': + return 'Audio'; + case 'fileInfo.file': + return 'File'; + case 'fileInfo.advanced': + return 'Avanzate'; + case 'fileInfo.codec': + return 'Codec'; + case 'fileInfo.resolution': + return 'Risoluzione'; + case 'fileInfo.bitrate': + return 'Bitrate'; + case 'fileInfo.frameRate': + return 'Frame Rate'; + case 'fileInfo.aspectRatio': + return 'Aspect Ratio'; + case 'fileInfo.profile': + return 'Profilo'; + case 'fileInfo.bitDepth': + return 'Profondità colore'; + case 'fileInfo.colorSpace': + return 'Spazio colore'; + case 'fileInfo.colorRange': + return 'Gamma colori'; + case 'fileInfo.colorPrimaries': + return 'Colori primari'; + case 'fileInfo.chromaSubsampling': + return 'Sottocampionamento cromatico'; + case 'fileInfo.channels': + return 'Canali'; + case 'fileInfo.path': + return 'Percorso'; + case 'fileInfo.size': + return 'Dimensione'; + case 'fileInfo.container': + return 'Contenitore'; + case 'fileInfo.duration': + return 'Durata'; + case 'fileInfo.optimizedForStreaming': + return 'Ottimizzato per lo streaming'; + case 'fileInfo.has64bitOffsets': + return 'Offset a 64-bit'; + case 'mediaMenu.markAsWatched': + return 'Segna come visto'; + case 'mediaMenu.markAsUnwatched': + return 'Segna come non visto'; + case 'mediaMenu.removeFromContinueWatching': + return 'Rimuovi da Continua a guardare'; + case 'mediaMenu.goToSeries': + return 'Vai alle serie'; + case 'mediaMenu.goToSeason': + return 'Vai alla stagione'; + case 'mediaMenu.shufflePlay': + return 'Riproduzione casuale'; + case 'mediaMenu.fileInfo': + return 'Info sul file'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, film'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, serie TV'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return 'visto'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '${percent} percento visto'; + case 'accessibility.mediaCardUnwatched': + return 'non visto'; + case 'accessibility.tapToPlay': + return 'Tocca per riprodurre'; + case 'tooltips.shufflePlay': + return 'Riproduzione casuale'; + case 'tooltips.markAsWatched': + return 'Segna come visto'; + case 'tooltips.markAsUnwatched': + return 'Segna come non visto'; + case 'videoControls.audioLabel': + return 'Audio'; + case 'videoControls.subtitlesLabel': + return 'Sottotitoli'; + case 'videoControls.resetToZero': + return 'Riporta a 0ms'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} riprodotto dopo'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} riprodotto prima'; + case 'videoControls.noOffset': + return 'Nessun offset'; + case 'videoControls.letterbox': + return 'Letterbox'; + case 'videoControls.fillScreen': + return 'Riempi schermo'; + case 'videoControls.stretch': + return 'Allunga'; + case 'videoControls.lockRotation': + return 'Blocca rotazione'; + case 'videoControls.unlockRotation': + return 'Sblocca rotazione'; + case 'videoControls.sleepTimer': + return 'Timer di spegnimento'; + case 'videoControls.timerActive': + return 'Timer attivo'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => + 'La riproduzione si interromperà tra ${duration}'; + case 'videoControls.sleepTimerCompleted': + return 'Timer di spegnimento completato - riproduzione in pausa'; + case 'videoControls.playButton': + return 'Riproduci'; + case 'videoControls.pauseButton': + return 'Pausa'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => 'Riavvolgi di ${seconds} secondi'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => 'Avanza di ${seconds} secondi'; + case 'videoControls.previousButton': + return 'Episodio precedente'; + case 'videoControls.nextButton': + return 'Episodio successivo'; + case 'videoControls.previousChapterButton': + return 'Capitolo precedente'; + case 'videoControls.nextChapterButton': + return 'Capitolo successivo'; + case 'videoControls.volumeButton': + return 'Volume'; + case 'videoControls.muteButton': + return 'Silenzia'; + case 'videoControls.unmuteButton': + return 'Riattiva audio'; + case 'videoControls.settingsButton': + return 'Impostazioni video'; + case 'videoControls.audioTrackButton': + return 'Tracce audio'; + case 'videoControls.subtitlesButton': + return 'Sottotitoli'; + case 'videoControls.chaptersButton': + return 'Capitoli'; + case 'videoControls.versionsButton': + return 'Versioni video'; + case 'videoControls.aspectRatioButton': + return 'Proporzioni'; + case 'videoControls.fullscreenButton': + return 'Attiva schermo intero'; + case 'videoControls.exitFullscreenButton': + return 'Esci da schermo intero'; + case 'videoControls.rotationLockButton': + return 'Blocco rotazione'; + case 'videoControls.timelineSlider': + return 'Timeline video'; + case 'videoControls.volumeSlider': + return 'Livello volume'; + case 'videoControls.backButton': + return 'Indietro'; + case 'userStatus.admin': + return 'Admin'; + case 'userStatus.restricted': + return 'Limitato'; + case 'userStatus.protected': + return 'Protetto'; + case 'userStatus.current': + return 'ATTUALE'; + case 'messages.markedAsWatched': + return 'Segna come visto'; + case 'messages.markedAsUnwatched': + return 'Segna come non visto'; + case 'messages.markedAsWatchedOffline': + return 'Segnato come visto (sincronizzato online)'; + case 'messages.markedAsUnwatchedOffline': + return 'Segnato come non visto (sincronizzato online)'; + case 'messages.removedFromContinueWatching': + return 'Rimosso da Continua a guardare'; + case 'messages.errorLoading': + return ({required Object error}) => 'Errore: ${error}'; + case 'messages.fileInfoNotAvailable': + return 'Informazioni sul file non disponibili'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => + 'Errore caricamento informazioni sul file: ${error}'; + case 'messages.errorLoadingSeries': + return 'Errore caricamento serie'; + case 'messages.errorLoadingSeason': + return 'Errore caricamento stagione'; + case 'messages.musicNotSupported': + return 'La riproduzione musicale non è ancora supportata'; + case 'messages.logsCleared': + return 'Log eliminati'; + case 'messages.logsCopied': + return 'Log copiati negli appunti'; + case 'messages.noLogsAvailable': + return 'Nessun log disponibile'; + case 'messages.libraryScanning': + return ({required Object title}) => 'Scansione "${title}"...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => + 'Scansione libreria iniziata per "${title}"'; + case 'messages.libraryScanFailed': + return ({required Object error}) => + 'Impossibile eseguire scansione della libreria: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => + 'Aggiornamento metadati per "${title}"...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => + 'Aggiornamento metadati per "${title}"'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => + 'Errore aggiornamento metadati: ${error}'; + case 'messages.noPlexToken': + return 'Nessun token Plex trovato. Riesegui l\'accesso.'; + case 'messages.logoutConfirm': + return 'Sei sicuro di volerti disconnettere?'; + case 'messages.noSeasonsFound': + return 'Nessuna stagione trovata'; + case 'messages.noEpisodesFound': + return 'Nessun episodio trovato nella prima stagione'; + case 'messages.noEpisodesFoundGeneral': + return 'Nessun episodio trovato'; + case 'messages.noResultsFound': + return 'Nessun risultato'; + case 'messages.sleepTimerSet': + return ({required Object label}) => + 'Imposta timer spegnimento per ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => + 'Impossibile passare a ${displayName}'; + case 'messages.noItemsAvailable': + return 'Nessun elemento disponibile'; + case 'messages.failedToCreatePlayQueue': + return 'Impossibile creare la coda di riproduzione'; + case 'messages.failedToCreatePlayQueueNoItems': + return 'Impossibile creare la coda di riproduzione - nessun elemento'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + 'Impossibile ${action}: ${error}'; + case 'messages.youAreOffline': + return 'Sei offline'; + case 'messages.offlineFeatureUnavailable': + return 'Questa funzione richiede una connessione internet. I tuoi download e le impostazioni sono ancora disponibili.'; + case 'profile.noUsersAvailable': + return 'Nessun utente disponibile'; + case 'subtitlingStyling.stylingOptions': + return 'Opzioni stile'; + case 'subtitlingStyling.fontSize': + return 'Dimensione'; + case 'subtitlingStyling.textColor': + return 'Colore testo'; + case 'subtitlingStyling.borderSize': + return 'Dimensione bordo'; + case 'subtitlingStyling.borderColor': + return 'Colore bordo'; + case 'subtitlingStyling.backgroundOpacity': + return 'Opacità sfondo'; + case 'subtitlingStyling.backgroundColor': + return 'Colore sfondo'; + case 'dialog.confirmAction': + return 'Conferma azione'; + case 'dialog.areYouSure': + return 'Sei sicuro di voler eseguire questa azione?'; + case 'dialog.cancel': + return 'Cancella'; + case 'dialog.playNow': + return 'Riproduci ora'; + case 'discover.title': + return 'Esplora'; + case 'discover.switchProfile': + return 'Cambia profilo'; + case 'discover.switchServer': + return 'Cambia server'; + case 'discover.logout': + return 'Disconnetti'; + case 'discover.noContentAvailable': + return 'Nessun contenuto disponibile'; + case 'discover.addMediaToLibraries': + return 'Aggiungi alcuni file multimediali alle tue librerie'; + case 'discover.continueWatching': + return 'Continua a guardare'; + case 'discover.recentlyAdded': + return 'Aggiunti di recente'; + case 'discover.play': + return 'Riproduci'; + case 'discover.resume': + return 'Riprendi'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return 'Pausa'; + case 'discover.overview': + return 'Panoramica'; + case 'discover.cast': + return 'Attori'; + case 'discover.seasons': + return 'Stagioni'; + case 'discover.studio': + return 'Studio'; + case 'discover.rating': + return 'Classificazione'; + case 'discover.watched': + return 'Guardato'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} episodi'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '${watched}/${total} guardati'; + case 'discover.movie': + return 'Film'; + case 'discover.tvShow': + return 'Serie TV'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '${minutes} minuti rimanenti'; + case 'errors.searchFailed': + return ({required Object error}) => 'Ricerca fallita: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => + 'Timeout connessione durante caricamento di ${context}'; + case 'errors.connectionFailed': + return 'Impossibile connettersi al server Plex.'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + 'Impossibile caricare ${context}: ${error}'; + case 'errors.noClientAvailable': + return 'Nessun client disponibile'; + case 'errors.authenticationFailed': + return ({required Object error}) => 'Autenticazione fallita: ${error}'; + case 'errors.couldNotLaunchUrl': + return 'Impossibile avviare URL di autenticazione'; + case 'errors.pleaseEnterToken': + return 'Inserisci token'; + case 'errors.invalidToken': + return 'Token non valido'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => 'Verifica token fallita: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => + 'Impossibile passare a ${displayName}'; + case 'errors.connectionFailedGeneric': + return 'Connessione fallita'; + case 'libraries.title': + return 'Librerie'; + case 'libraries.scanLibraryFiles': + return 'Scansiona file libreria'; + case 'libraries.scanLibrary': + return 'Scansiona libreria'; + case 'libraries.analyze': + return 'Analizza'; + case 'libraries.analyzeLibrary': + return 'Analizza libreria'; + case 'libraries.refreshMetadata': + return 'Aggiorna metadati'; + case 'libraries.emptyTrash': + return 'Svuota cestino'; + case 'libraries.emptyingTrash': + return ({required Object title}) => + 'Svuotamento cestino per "${title}"...'; + case 'libraries.trashEmptied': + return ({required Object title}) => 'Cestino svuotato per "${title}"'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => + 'Impossibile svuotare cestino: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => 'Analisi "${title}"...'; + case 'libraries.analysisStarted': + return ({required Object title}) => 'Analisi iniziata per "${title}"'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => + 'Impossibile analizzare libreria: ${error}'; + case 'libraries.noLibrariesFound': + return 'Nessuna libreria trovata'; + case 'libraries.thisLibraryIsEmpty': + return 'Questa libreria è vuota'; + case 'libraries.all': + return 'Tutto'; + case 'libraries.clearAll': + return 'Cancella tutto'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => + 'Sei sicuro di voler scansionare "${title}"?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => + 'Sei sicuro di voler analizzare "${title}"?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => + 'Sei sicuro di voler aggiornare i metadati per "${title}"?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => + 'Sei sicuro di voler svuotare il cestino per "${title}"?'; + case 'libraries.manageLibraries': + return 'Gestisci librerie'; + case 'libraries.sort': + return 'Ordina'; + case 'libraries.sortBy': + return 'Ordina per'; + case 'libraries.filters': + return 'Filtri'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => + 'Caricamento librerie... (${count} oggetti caricati)'; + case 'libraries.confirmActionMessage': + return 'Sei sicuro di voler eseguire questa azione?'; + case 'libraries.showLibrary': + return 'Mostra libreria'; + case 'libraries.hideLibrary': + return 'Nascondi libreria'; + case 'libraries.libraryOptions': + return 'Opzioni libreria'; + case 'libraries.content': + return 'contenuto della libreria'; + case 'libraries.selectLibrary': + return 'Seleziona libreria'; + case 'libraries.filtersWithCount': + return ({required Object count}) => 'Filtri (${count})'; + case 'libraries.noRecommendations': + return 'Nessun consiglio disponibile'; + case 'libraries.noCollections': + return 'Nessuna raccolta in questa libreria'; + case 'libraries.noFoldersFound': + return 'Nessuna cartella trovata'; + case 'libraries.folders': + return 'cartelle'; + case 'libraries.tabs.recommended': + return 'Consigliati'; + case 'libraries.tabs.browse': + return 'Esplora'; + case 'libraries.tabs.collections': + return 'Raccolte'; + case 'libraries.tabs.playlists': + return 'Playlist'; + case 'libraries.groupings.all': + return 'Tutti'; + case 'libraries.groupings.movies': + return 'Film'; + case 'libraries.groupings.shows': + return 'Serie TV'; + case 'libraries.groupings.seasons': + return 'Stagioni'; + case 'libraries.groupings.episodes': + return 'Episodi'; + case 'libraries.groupings.folders': + return 'Cartelle'; + case 'about.title': + return 'Informazioni'; + case 'about.openSourceLicenses': + return 'Licenze Open Source'; + case 'about.versionLabel': + return ({required Object version}) => 'Versione ${version}'; + case 'about.appDescription': + return 'Un bellissimo client Plex per Flutter'; + case 'about.viewLicensesDescription': + return 'Visualizza le licenze delle librerie di terze parti'; + case 'serverSelection.connectingToServer': + return 'Connessione al server...'; + case 'serverSelection.connectingToAllServers': + return 'Connessione a tutti i server...'; + case 'serverSelection.connectToAllServers': + return 'Connetti a tutti i server'; + case 'serverSelection.allServerConnectionsFailed': + return 'Impossibile connettersi a nessun server. Controlla la tua rete e riprova.'; + case 'serverSelection.foundServers': + return ({required Object count}) => 'Trovati ${count} server'; + case 'serverSelection.orSelectIndividually': + return 'Oppure seleziona un server singolarmente:'; + case 'serverSelection.serverDebugCopied': + return 'Dati di debug del server copiati negli appunti'; + case 'serverSelection.copyDebugData': + return 'Copia dati di debug'; + case 'serverSelection.noServersFound': + return 'Nessun server trovato'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + 'Nessun server trovato per ${username} (${email})'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + 'Trovato ${count} server con dati difettosi. Nessun server valido disponibile.'; + case 'serverSelection.incompleteServerInfo': + return 'Alcuni server presentano informazioni incomplete e sono stati ignorati. Controlla il tuo account Plex.tv.'; + case 'serverSelection.incompleteConnectionInfo': + return 'Le informazioni di connessione al server sono incomplete. Riprova.'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => + 'Le informazioni sul server sono errate: ${message}'; + case 'serverSelection.networkConnectionFailed': + return 'Connessione di rete non riuscita. Controlla la tua connessione Internet e riprova.'; + case 'serverSelection.authenticationFailed': + return 'Autenticazione fallita. Effettua nuovamente l\'accesso.'; + case 'serverSelection.plexServiceUnavailable': + return 'Servizio Plex non disponibile. Riprova più tardi.'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => + 'Impossibile caricare i server: ${error}'; + case 'hubDetail.title': + return 'Titolo'; + case 'hubDetail.releaseYear': + return 'Anno rilascio'; + case 'hubDetail.dateAdded': + return 'Data aggiunta'; + case 'hubDetail.rating': + return 'Valutazione'; + case 'hubDetail.noItemsFound': + return 'Nessun elemento trovato'; + case 'logs.title': + return 'Registro'; + case 'logs.clearLogs': + return 'Cancella log'; + case 'logs.copyLogs': + return 'Copia log'; + case 'logs.exportLogs': + return 'Esporta log'; + case 'logs.noLogsToShow': + return 'Nessun log da mostrare'; + case 'logs.error': + return 'Errore:'; + case 'logs.stackTrace': + return 'Traccia dello stack:'; + case 'licenses.relatedPackages': + return 'Pacchetti correlati'; + case 'licenses.license': + return 'Licenza'; + case 'licenses.licenseNumber': + return ({required Object number}) => 'Licenza ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} licenze'; + case 'navigation.home': + return 'Home'; + case 'navigation.search': + return 'Cerca'; + case 'navigation.libraries': + return 'Librerie'; + case 'navigation.settings': + return 'Impostazioni'; + case 'navigation.downloads': + return 'Download'; + case 'downloads.title': + return 'Download'; + case 'downloads.manage': + return 'Gestisci'; + case 'downloads.tvShows': + return 'Serie TV'; + case 'downloads.movies': + return 'Film'; + case 'downloads.queue': + return 'Coda'; + case 'downloads.noDownloads': + return 'Nessun download'; + case 'downloads.noDownloadsDescription': + return 'I contenuti scaricati appariranno qui per la visualizzazione offline'; + case 'downloads.browseContent': + return 'Sfoglia contenuti'; + case 'downloads.downloading': + return 'Download in corso'; + case 'downloads.downloaded': + return 'Scaricato'; + case 'downloads.paused': + return 'In pausa'; + case 'downloads.failed': + return 'Fallito'; + case 'downloads.queued': + return 'In coda'; + case 'downloads.downloadNow': + return 'Scarica'; + case 'downloads.deleteDownload': + return 'Elimina download'; + case 'downloads.cancelDownload': + return 'Annulla download'; + case 'downloads.pauseDownload': + return 'Metti in pausa download'; + case 'downloads.resumeDownload': + return 'Riprendi download'; + case 'downloads.retryDownload': + return 'Riprova download'; + case 'downloads.downloadComplete': + return 'Download completato'; + case 'downloads.downloadFailed': + return 'Download fallito'; + case 'downloads.downloadCancelled': + return 'Download annullato'; + case 'downloads.downloadQueued': + return 'Download in coda'; + case 'downloads.episodesQueued': + return ({required Object count}) => + '${count} episodi in coda per il download'; + case 'downloads.downloadDeleted': + return 'Download eliminato'; + case 'downloads.deleteConfirmTitle': + return 'Eliminare il download?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + 'Sei sicuro di voler eliminare "${title}"? Il file scaricato verrà rimosso dal tuo dispositivo.'; + case 'downloads.deleteConfirmMessage': + return 'Il file scaricato verrà rimosso dal tuo dispositivo.'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => 'Eliminazione di ${title}... (${current} di ${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => 'Eliminazione di ${title}...'; + case 'downloads.storageUsed': + return 'Spazio utilizzato'; + case 'downloads.availableOffline': + return 'Disponibile offline'; + case 'downloads.downloadingSeason': + return 'Download stagione in corso'; + case 'downloads.downloadingEpisode': + return 'Download episodio in corso'; + case 'offline.youAreOffline': + return 'Sei offline'; + case 'offline.offlineMode': + return 'Modalità offline'; + case 'offline.backOnline': + return 'Di nuovo online'; + case 'offline.showingDownloadedContent': + return 'Visualizzazione solo contenuti scaricati'; + case 'offline.noInternetConnection': + return 'Nessuna connessione internet'; + case 'offline.serversUnreachable': + return 'Server Plex non raggiungibili'; + case 'offline.connectToDownload': + return 'Connettiti a internet per scaricare contenuti'; + case 'playlists.title': + return 'Playlist'; + case 'playlists.noPlaylists': + return 'Nessuna playlist trovata'; + case 'playlists.create': + return 'Crea playlist'; + case 'playlists.newPlaylist': + return 'Nuova playlist'; + case 'playlists.playlistName': + return 'Nome playlist'; + case 'playlists.enterPlaylistName': + return 'Inserisci nome playlist'; + case 'playlists.edit': + return 'Modifica playlist'; + case 'playlists.delete': + return 'Elimina playlist'; + case 'playlists.addTo': + return 'Aggiungi a playlist'; + case 'playlists.addItems': + return 'Aggiungi elementi'; + case 'playlists.removeItem': + return 'Rimuovi da playlist'; + case 'playlists.clearPlaylist': + return 'Svuota playlist'; + case 'playlists.playAll': + return 'Riproduci tutto'; + case 'playlists.shuffle': + return 'Casuale'; + case 'playlists.smartPlaylist': + return 'Playlist intelligente'; + case 'playlists.regularPlaylist': + return 'Playlist normale'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} elementi'; + case 'playlists.oneItem': + return '1 elemento'; + case 'playlists.emptyPlaylist': + return 'Questa playlist è vuota'; + case 'playlists.deleteConfirm': + return 'Eliminare playlist?'; + case 'playlists.deleteMessage': + return ({required Object name}) => + 'Sei sicuro di voler eliminare "${name}"?'; + case 'playlists.created': + return 'Playlist creata'; + case 'playlists.updated': + return 'Playlist aggiornata'; + case 'playlists.deleted': + return 'Playlist eliminata'; + case 'playlists.itemAdded': + return 'Aggiunto alla playlist'; + case 'playlists.itemRemoved': + return 'Rimosso dalla playlist'; + case 'playlists.selectPlaylist': + return 'Seleziona playlist'; + case 'playlists.createNewPlaylist': + return 'Crea nuova playlist'; + case 'playlists.errorCreating': + return 'Errore durante la creazione della playlist'; + case 'playlists.errorDeleting': + return 'Errore durante l\'eliminazione della playlist'; + case 'playlists.errorLoading': + return 'Errore durante il caricamento delle playlist'; + case 'playlists.errorAdding': + return 'Errore durante l\'aggiunta alla playlist'; + case 'playlists.errorReordering': + return 'Errore durante il riordino dell\'elemento della playlist'; + case 'playlists.errorRemoving': + return 'Errore durante la rimozione dalla playlist'; + case 'playlists.playlist': + return 'Playlist'; + case 'collections.title': + return 'Raccolte'; + case 'collections.collection': + return 'Raccolta'; + case 'collections.empty': + return 'La raccolta è vuota'; + case 'collections.noItems': + return 'Nessun elemento in questa raccolta'; + case 'collections.unknownLibrarySection': + return 'Impossibile eliminare: sezione libreria sconosciuta'; + case 'collections.deleteCollection': + return 'Elimina raccolta'; + case 'collections.deleteConfirm': + return ({required Object title}) => + 'Sei sicuro di voler eliminare "${title}"? Questa azione non può essere annullata.'; + case 'collections.deleted': + return 'Raccolta eliminata'; + case 'collections.deleteFailed': + return 'Impossibile eliminare la raccolta'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => + 'Impossibile eliminare la raccolta: ${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => + 'Impossibile caricare gli elementi della raccolta: ${error}'; + case 'collections.addTo': + return 'Aggiungi alla raccolta'; + case 'collections.selectCollection': + return 'Seleziona raccolta'; + case 'collections.createNewCollection': + return 'Crea nuova raccolta'; + case 'collections.collectionName': + return 'Nome raccolta'; + case 'collections.enterCollectionName': + return 'Inserisci nome raccolta'; + case 'collections.addedToCollection': + return 'Aggiunto alla raccolta'; + case 'collections.errorAddingToCollection': + return 'Errore nell\'aggiunta alla raccolta'; + case 'collections.created': + return 'Raccolta creata'; + case 'collections.removeFromCollection': + return 'Rimuovi dalla raccolta'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => + 'Rimuovere "${title}" da questa raccolta?'; + case 'collections.removedFromCollection': + return 'Rimosso dalla raccolta'; + case 'collections.removeFromCollectionFailed': + return 'Impossibile rimuovere dalla raccolta'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => + 'Errore durante la rimozione dalla raccolta: ${error}'; + default: + return null; + } + } } extension on _StringsNl { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return 'Laden...'; - case 'auth.signInWithPlex': return 'Inloggen met Plex'; - case 'auth.showQRCode': return 'Toon QR-code'; - case 'auth.cancel': return 'Annuleren'; - case 'auth.authenticate': return 'Authenticeren'; - case 'auth.retry': return 'Opnieuw proberen'; - case 'auth.debugEnterToken': return 'Debug: Voer Plex Token in'; - case 'auth.plexTokenLabel': return 'Plex Authenticatietoken'; - case 'auth.plexTokenHint': return 'Voer je Plex.tv token in'; - case 'auth.authenticationTimeout': return 'Authenticatie verlopen. Probeer opnieuw.'; - case 'auth.scanQRCodeInstruction': return 'Scan deze QR-code met een apparaat dat is ingelogd op Plex om te authenticeren.'; - case 'auth.waitingForAuth': return 'Wachten op authenticatie...\nVoltooi het inloggen in je browser.'; - case 'common.cancel': return 'Annuleren'; - case 'common.save': return 'Opslaan'; - case 'common.close': return 'Sluiten'; - case 'common.clear': return 'Wissen'; - case 'common.reset': return 'Resetten'; - case 'common.later': return 'Later'; - case 'common.submit': return 'Verzenden'; - case 'common.confirm': return 'Bevestigen'; - case 'common.retry': return 'Opnieuw proberen'; - case 'common.playNow': return 'Nu afspelen'; - case 'common.logout': return 'Uitloggen'; - case 'common.online': return 'Online'; - case 'common.offline': return 'Offline'; - case 'common.owned': return 'Eigendom'; - case 'common.shared': return 'Gedeeld'; - case 'common.current': return 'HUIDIG'; - case 'common.unknown': return 'Onbekend'; - case 'common.refresh': return 'Vernieuwen'; - case 'common.yes': return 'Ja'; - case 'common.no': return 'Nee'; - case 'common.server': return 'Server'; - case 'common.delete': return 'Verwijderen'; - case 'common.shuffle': return 'Willekeurig'; - case 'common.addTo': return 'Toevoegen aan...'; - case 'screens.licenses': return 'Licenties'; - case 'screens.selectServer': return 'Selecteer server'; - case 'screens.switchProfile': return 'Wissel van profiel'; - case 'screens.subtitleStyling': return 'Ondertitel opmaak'; - case 'screens.search': return 'Zoeken'; - case 'screens.logs': return 'Logbestanden'; - case 'update.available': return 'Update beschikbaar'; - case 'update.versionAvailable': return ({required Object version}) => 'Versie ${version} is beschikbaar'; - case 'update.currentVersion': return ({required Object version}) => 'Huidig: ${version}'; - case 'update.skipVersion': return 'Deze versie overslaan'; - case 'update.viewRelease': return 'Bekijk release'; - case 'update.latestVersion': return 'Je hebt de nieuwste versie'; - case 'update.checkFailed': return 'Kon niet controleren op updates'; - case 'settings.title': return 'Instellingen'; - case 'settings.language': return 'Taal'; - case 'settings.theme': return 'Thema'; - case 'settings.appearance': return 'Uiterlijk'; - case 'settings.videoPlayback': return 'Video afspelen'; - case 'settings.shufflePlay': return 'Willekeurig afspelen'; - case 'settings.advanced': return 'Geavanceerd'; - case 'settings.useSeasonPostersDescription': return 'Toon seizoenposter in plaats van serieposter voor afleveringen'; - case 'settings.showHeroSectionDescription': return 'Toon uitgelichte inhoud carrousel op startscherm'; - case 'settings.secondsLabel': return 'Seconden'; - case 'settings.minutesLabel': return 'Minuten'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => 'Voer duur in (${min}-${max})'; - case 'settings.systemTheme': return 'Systeem'; - case 'settings.systemThemeDescription': return 'Volg systeeminstellingen'; - case 'settings.lightTheme': return 'Licht'; - case 'settings.darkTheme': return 'Donker'; - case 'settings.libraryDensity': return 'Bibliotheek dichtheid'; - case 'settings.compact': return 'Compact'; - case 'settings.compactDescription': return 'Kleinere kaarten, meer items zichtbaar'; - case 'settings.normal': return 'Normaal'; - case 'settings.normalDescription': return 'Standaard grootte'; - case 'settings.comfortable': return 'Comfortabel'; - case 'settings.comfortableDescription': return 'Grotere kaarten, minder items zichtbaar'; - case 'settings.viewMode': return 'Weergavemodus'; - case 'settings.gridView': return 'Raster'; - case 'settings.gridViewDescription': return 'Items weergeven in een rasterindeling'; - case 'settings.listView': return 'Lijst'; - case 'settings.listViewDescription': return 'Items weergeven in een lijstindeling'; - case 'settings.useSeasonPosters': return 'Gebruik seizoenposters'; - case 'settings.showHeroSection': return 'Toon hoofdsectie'; - case 'settings.hardwareDecoding': return 'Hardware decodering'; - case 'settings.hardwareDecodingDescription': return 'Gebruik hardware versnelling indien beschikbaar'; - case 'settings.bufferSize': return 'Buffer grootte'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return 'Ondertitel opmaak'; - case 'settings.subtitleStylingDescription': return 'Pas ondertitel uiterlijk aan'; - case 'settings.smallSkipDuration': return 'Korte skip duur'; - case 'settings.largeSkipDuration': return 'Lange skip duur'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} seconden'; - case 'settings.defaultSleepTimer': return 'Standaard slaap timer'; - case 'settings.minutesUnit': return ({required Object minutes}) => 'bij ${minutes} minuten'; - case 'settings.rememberTrackSelections': return 'Onthoud track selecties per serie/film'; - case 'settings.rememberTrackSelectionsDescription': return 'Bewaar automatisch audio- en ondertiteltaalvoorkeuren wanneer je tracks wijzigt tijdens afspelen'; - case 'settings.unwatchedOnly': return 'Alleen ongekeken'; - case 'settings.unwatchedOnlyDescription': return 'Alleen ongekeken afleveringen opnemen in willekeurige wachtrij'; - case 'settings.shuffleOrderNavigation': return 'Willekeurige volgorde navigatie'; - case 'settings.shuffleOrderNavigationDescription': return 'Volgende/vorige knoppen volgen willekeurige volgorde'; - case 'settings.loopShuffleQueue': return 'Herhaal willekeurige wachtrij'; - case 'settings.loopShuffleQueueDescription': return 'Start wachtrij opnieuw bij het einde'; - case 'settings.videoPlayerControls': return 'Videospeler bediening'; - case 'settings.keyboardShortcuts': return 'Toetsenbord sneltoetsen'; - case 'settings.keyboardShortcutsDescription': return 'Pas toetsenbord sneltoetsen aan'; - case 'settings.debugLogging': return 'Debug logging'; - case 'settings.debugLoggingDescription': return 'Schakel gedetailleerde logging in voor probleemoplossing'; - case 'settings.viewLogs': return 'Bekijk logs'; - case 'settings.viewLogsDescription': return 'Bekijk applicatie logs'; - case 'settings.clearCache': return 'Cache wissen'; - case 'settings.clearCacheDescription': return 'Dit wist alle gecachte afbeeldingen en gegevens. De app kan langer duren om inhoud te laden na het wissen van de cache.'; - case 'settings.clearCacheSuccess': return 'Cache succesvol gewist'; - case 'settings.resetSettings': return 'Instellingen resetten'; - case 'settings.resetSettingsDescription': return 'Dit reset alle instellingen naar hun standaard waarden. Deze actie kan niet ongedaan gemaakt worden.'; - case 'settings.resetSettingsSuccess': return 'Instellingen succesvol gereset'; - case 'settings.shortcutsReset': return 'Sneltoetsen gereset naar standaard'; - case 'settings.about': return 'Over'; - case 'settings.aboutDescription': return 'App informatie en licenties'; - case 'settings.updates': return 'Updates'; - case 'settings.updateAvailable': return 'Update beschikbaar'; - case 'settings.checkForUpdates': return 'Controleer op updates'; - case 'settings.validationErrorEnterNumber': return 'Voer een geldig nummer in'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => 'Duur moet tussen ${min} en ${max} ${unit} zijn'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => 'Sneltoets al toegewezen aan ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => 'Sneltoets bijgewerkt voor ${action}'; - case 'settings.autoSkip': return 'Automatisch Overslaan'; - case 'settings.autoSkipIntro': return 'Intro Automatisch Overslaan'; - case 'settings.autoSkipIntroDescription': return 'Intro-markeringen na enkele seconden automatisch overslaan'; - case 'settings.autoSkipCredits': return 'Credits Automatisch Overslaan'; - case 'settings.autoSkipCreditsDescription': return 'Credits automatisch overslaan en volgende aflevering afspelen'; - case 'settings.autoSkipDelay': return 'Vertraging Automatisch Overslaan'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => '${seconds} seconden wachten voor automatisch overslaan'; - case 'search.hint': return 'Zoek films, series, muziek...'; - case 'search.tryDifferentTerm': return 'Probeer een andere zoekterm'; - case 'search.searchYourMedia': return 'Zoek in je media'; - case 'search.enterTitleActorOrKeyword': return 'Voer een titel, acteur of trefwoord in'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => 'Stel sneltoets in voor ${actionName}'; - case 'hotkeys.clearShortcut': return 'Wis sneltoets'; - case 'pinEntry.enterPin': return 'Voer PIN in'; - case 'pinEntry.showPin': return 'Toon PIN'; - case 'pinEntry.hidePin': return 'Verberg PIN'; - case 'fileInfo.title': return 'Bestand info'; - case 'fileInfo.video': return 'Video'; - case 'fileInfo.audio': return 'Audio'; - case 'fileInfo.file': return 'Bestand'; - case 'fileInfo.advanced': return 'Geavanceerd'; - case 'fileInfo.codec': return 'Codec'; - case 'fileInfo.resolution': return 'Resolutie'; - case 'fileInfo.bitrate': return 'Bitrate'; - case 'fileInfo.frameRate': return 'Frame rate'; - case 'fileInfo.aspectRatio': return 'Beeldverhouding'; - case 'fileInfo.profile': return 'Profiel'; - case 'fileInfo.bitDepth': return 'Bit diepte'; - case 'fileInfo.colorSpace': return 'Kleurruimte'; - case 'fileInfo.colorRange': return 'Kleurbereik'; - case 'fileInfo.colorPrimaries': return 'Kleurprimaires'; - case 'fileInfo.chromaSubsampling': return 'Chroma subsampling'; - case 'fileInfo.channels': return 'Kanalen'; - case 'fileInfo.path': return 'Pad'; - case 'fileInfo.size': return 'Grootte'; - case 'fileInfo.container': return 'Container'; - case 'fileInfo.duration': return 'Duur'; - case 'fileInfo.optimizedForStreaming': return 'Geoptimaliseerd voor streaming'; - case 'fileInfo.has64bitOffsets': return '64-bit Offsets'; - case 'mediaMenu.markAsWatched': return 'Markeer als gekeken'; - case 'mediaMenu.markAsUnwatched': return 'Markeer als ongekeken'; - case 'mediaMenu.removeFromContinueWatching': return 'Verwijder uit Doorgaan met kijken'; - case 'mediaMenu.goToSeries': return 'Ga naar serie'; - case 'mediaMenu.goToSeason': return 'Ga naar seizoen'; - case 'mediaMenu.shufflePlay': return 'Willekeurig afspelen'; - case 'mediaMenu.fileInfo': return 'Bestand info'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, film'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV-serie'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return 'bekeken'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} procent bekeken'; - case 'accessibility.mediaCardUnwatched': return 'niet bekeken'; - case 'accessibility.tapToPlay': return 'Tik om af te spelen'; - case 'tooltips.shufflePlay': return 'Willekeurig afspelen'; - case 'tooltips.markAsWatched': return 'Markeer als gekeken'; - case 'tooltips.markAsUnwatched': return 'Markeer als ongekeken'; - case 'videoControls.audioLabel': return 'Audio'; - case 'videoControls.subtitlesLabel': return 'Ondertitels'; - case 'videoControls.resetToZero': return 'Reset naar 0ms'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} speelt later af'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} speelt eerder af'; - case 'videoControls.noOffset': return 'Geen offset'; - case 'videoControls.letterbox': return 'Letterbox'; - case 'videoControls.fillScreen': return 'Vul scherm'; - case 'videoControls.stretch': return 'Uitrekken'; - case 'videoControls.lockRotation': return 'Vergrendel rotatie'; - case 'videoControls.unlockRotation': return 'Ontgrendel rotatie'; - case 'videoControls.sleepTimer': return 'Slaaptimer'; - case 'videoControls.timerActive': return 'Timer actief'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}'; - case 'videoControls.sleepTimerCompleted': return 'Slaaptimer voltooid - afspelen gepauzeerd'; - case 'videoControls.playButton': return 'Afspelen'; - case 'videoControls.pauseButton': return 'Pauzeren'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Terugspoelen ${seconds} seconden'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Vooruitspoelen ${seconds} seconden'; - case 'videoControls.previousButton': return 'Vorige aflevering'; - case 'videoControls.nextButton': return 'Volgende aflevering'; - case 'videoControls.previousChapterButton': return 'Vorig hoofdstuk'; - case 'videoControls.nextChapterButton': return 'Volgend hoofdstuk'; - case 'videoControls.volumeButton': return 'Volume'; - case 'videoControls.muteButton': return 'Dempen'; - case 'videoControls.unmuteButton': return 'Dempen opheffen'; - case 'videoControls.settingsButton': return 'Video-instellingen'; - case 'videoControls.audioTrackButton': return 'Audiosporen'; - case 'videoControls.subtitlesButton': return 'Ondertitels'; - case 'videoControls.chaptersButton': return 'Hoofdstukken'; - case 'videoControls.versionsButton': return 'Videoversies'; - case 'videoControls.aspectRatioButton': return 'Beeldverhouding'; - case 'videoControls.fullscreenButton': return 'Volledig scherm activeren'; - case 'videoControls.exitFullscreenButton': return 'Volledig scherm verlaten'; - case 'videoControls.rotationLockButton': return 'Rotatievergrendeling'; - case 'videoControls.timelineSlider': return 'Videotijdlijn'; - case 'videoControls.volumeSlider': return 'Volumeniveau'; - case 'videoControls.backButton': return 'Terug'; - case 'userStatus.admin': return 'Beheerder'; - case 'userStatus.restricted': return 'Beperkt'; - case 'userStatus.protected': return 'Beschermd'; - case 'userStatus.current': return 'HUIDIG'; - case 'messages.markedAsWatched': return 'Gemarkeerd als gekeken'; - case 'messages.markedAsUnwatched': return 'Gemarkeerd als ongekeken'; - case 'messages.removedFromContinueWatching': return 'Verwijderd uit Doorgaan met kijken'; - case 'messages.errorLoading': return ({required Object error}) => 'Fout: ${error}'; - case 'messages.fileInfoNotAvailable': return 'Bestand informatie niet beschikbaar'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Fout bij laden bestand info: ${error}'; - case 'messages.errorLoadingSeries': return 'Fout bij laden serie'; - case 'messages.errorLoadingSeason': return 'Fout bij laden seizoen'; - case 'messages.musicNotSupported': return 'Muziek afspelen wordt nog niet ondersteund'; - case 'messages.logsCleared': return 'Logs gewist'; - case 'messages.logsCopied': return 'Logs gekopieerd naar klembord'; - case 'messages.noLogsAvailable': return 'Geen logs beschikbaar'; - case 'messages.libraryScanning': return ({required Object title}) => 'Scannen "${title}"...'; - case 'messages.libraryScanStarted': return ({required Object title}) => 'Bibliotheek scan gestart voor "${title}"'; - case 'messages.libraryScanFailed': return ({required Object error}) => 'Kon bibliotheek niet scannen: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => 'Metadata vernieuwen voor "${title}"...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => 'Metadata vernieuwen gestart voor "${title}"'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => 'Kon metadata niet vernieuwen: ${error}'; - case 'messages.noPlexToken': return 'Geen Plex token gevonden. Log opnieuw in.'; - case 'messages.logoutConfirm': return 'Weet je zeker dat je wilt uitloggen?'; - case 'messages.noSeasonsFound': return 'Geen seizoenen gevonden'; - case 'messages.noEpisodesFound': return 'Geen afleveringen gevonden in eerste seizoen'; - case 'messages.noEpisodesFoundGeneral': return 'Geen afleveringen gevonden'; - case 'messages.noResultsFound': return 'Geen resultaten gevonden'; - case 'messages.sleepTimerSet': return ({required Object label}) => 'Slaap timer ingesteld voor ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => 'Kon niet wisselen naar ${displayName}'; - case 'messages.noItemsAvailable': return 'Geen items beschikbaar'; - case 'messages.failedToCreatePlayQueue': return 'Kan afspeelwachtrij niet maken'; - case 'messages.failedToCreatePlayQueueNoItems': return 'Kan afspeelwachtrij niet maken - geen items'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => 'Afspelen van ${action} mislukt: ${error}'; - case 'profile.noUsersAvailable': return 'Geen gebruikers beschikbaar'; - case 'subtitlingStyling.stylingOptions': return 'Opmaak opties'; - case 'subtitlingStyling.fontSize': return 'Lettergrootte'; - case 'subtitlingStyling.textColor': return 'Tekstkleur'; - case 'subtitlingStyling.borderSize': return 'Rand grootte'; - case 'subtitlingStyling.borderColor': return 'Randkleur'; - case 'subtitlingStyling.backgroundOpacity': return 'Achtergrond transparantie'; - case 'subtitlingStyling.backgroundColor': return 'Achtergrondkleur'; - case 'dialog.confirmAction': return 'Bevestig actie'; - case 'dialog.areYouSure': return 'Weet je zeker dat je deze actie wilt uitvoeren?'; - case 'dialog.cancel': return 'Annuleren'; - case 'dialog.playNow': return 'Nu afspelen'; - case 'discover.title': return 'Ontdekken'; - case 'discover.switchProfile': return 'Wissel van profiel'; - case 'discover.switchServer': return 'Wissel van server'; - case 'discover.logout': return 'Uitloggen'; - case 'discover.noContentAvailable': return 'Geen inhoud beschikbaar'; - case 'discover.addMediaToLibraries': return 'Voeg wat media toe aan je bibliotheken'; - case 'discover.continueWatching': return 'Verder kijken'; - case 'discover.recentlyAdded': return 'Recent toegevoegd'; - case 'discover.play': return 'Afspelen'; - case 'discover.resume': return 'Hervatten'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => 'Speel S${season}, E${episode}'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => 'Hervat S${season}, E${episode}'; - case 'discover.pause': return 'Pauzeren'; - case 'discover.overview': return 'Overzicht'; - case 'discover.cast': return 'Acteurs'; - case 'discover.seasons': return 'Seizoenen'; - case 'discover.studio': return 'Studio'; - case 'discover.rating': return 'Leeftijd'; - case 'discover.watched': return 'Bekeken'; - case 'discover.episodeCount': return ({required Object count}) => '${count} afleveringen'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '${watched}/${total} gekeken'; - case 'discover.movie': return 'Film'; - case 'discover.tvShow': return 'TV Serie'; - case 'discover.minutesLeft': return ({required Object minutes}) => '${minutes} min over'; - case 'errors.searchFailed': return ({required Object error}) => 'Zoeken mislukt: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => 'Verbinding time-out tijdens laden ${context}'; - case 'errors.connectionFailed': return 'Kan geen verbinding maken met Plex server'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => 'Kon ${context} niet laden: ${error}'; - case 'errors.noClientAvailable': return 'Geen client beschikbaar'; - case 'errors.authenticationFailed': return ({required Object error}) => 'Authenticatie mislukt: ${error}'; - case 'errors.couldNotLaunchUrl': return 'Kon auth URL niet openen'; - case 'errors.pleaseEnterToken': return 'Voer een token in'; - case 'errors.invalidToken': return 'Ongeldig token'; - case 'errors.failedToVerifyToken': return ({required Object error}) => 'Kon token niet verifiëren: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => 'Kon niet wisselen naar ${displayName}'; - case 'errors.connectionFailedGeneric': return 'Verbinding mislukt'; - case 'libraries.title': return 'Bibliotheken'; - case 'libraries.scanLibraryFiles': return 'Scan bibliotheek bestanden'; - case 'libraries.scanLibrary': return 'Scan bibliotheek'; - case 'libraries.analyze': return 'Analyseren'; - case 'libraries.analyzeLibrary': return 'Analyseer bibliotheek'; - case 'libraries.refreshMetadata': return 'Vernieuw metadata'; - case 'libraries.emptyTrash': return 'Prullenbak legen'; - case 'libraries.emptyingTrash': return ({required Object title}) => 'Prullenbak legen voor "${title}"...'; - case 'libraries.trashEmptied': return ({required Object title}) => 'Prullenbak geleegd voor "${title}"'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => 'Kon prullenbak niet legen: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => 'Analyseren "${title}"...'; - case 'libraries.analysisStarted': return ({required Object title}) => 'Analyse gestart voor "${title}"'; - case 'libraries.failedToAnalyze': return ({required Object error}) => 'Kon bibliotheek niet analyseren: ${error}'; - case 'libraries.noLibrariesFound': return 'Geen bibliotheken gevonden'; - case 'libraries.thisLibraryIsEmpty': return 'Deze bibliotheek is leeg'; - case 'libraries.all': return 'Alles'; - case 'libraries.clearAll': return 'Alles wissen'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => 'Weet je zeker dat je "${title}" wilt scannen?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => 'Weet je zeker dat je "${title}" wilt analyseren?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => 'Weet je zeker dat je metadata wilt vernieuwen voor "${title}"?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => 'Weet je zeker dat je de prullenbak wilt legen voor "${title}"?'; - case 'libraries.manageLibraries': return 'Beheer bibliotheken'; - case 'libraries.sort': return 'Sorteren'; - case 'libraries.sortBy': return 'Sorteer op'; - case 'libraries.filters': return 'Filters'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => 'Bibliotheek laden... (${count} items geladen)'; - case 'libraries.confirmActionMessage': return 'Weet je zeker dat je deze actie wilt uitvoeren?'; - case 'libraries.showLibrary': return 'Toon bibliotheek'; - case 'libraries.hideLibrary': return 'Verberg bibliotheek'; - case 'libraries.libraryOptions': return 'Bibliotheek opties'; - case 'libraries.content': return 'bibliotheekinhoud'; - case 'libraries.selectLibrary': return 'Bibliotheek kiezen'; - case 'libraries.filtersWithCount': return ({required Object count}) => 'Filters (${count})'; - case 'libraries.noRecommendations': return 'Geen aanbevelingen beschikbaar'; - case 'libraries.noCollections': return 'Geen collecties in deze bibliotheek'; - case 'libraries.noFoldersFound': return 'Geen mappen gevonden'; - case 'libraries.folders': return 'mappen'; - case 'libraries.tabs.recommended': return 'Aanbevolen'; - case 'libraries.tabs.browse': return 'Bladeren'; - case 'libraries.tabs.collections': return 'Collecties'; - case 'libraries.tabs.playlists': return 'Afspeellijsten'; - case 'libraries.groupings.all': return 'Alles'; - case 'libraries.groupings.movies': return 'Films'; - case 'libraries.groupings.shows': return 'Series'; - case 'libraries.groupings.seasons': return 'Seizoenen'; - case 'libraries.groupings.episodes': return 'Afleveringen'; - case 'libraries.groupings.folders': return 'Mappen'; - case 'about.title': return 'Over'; - case 'about.openSourceLicenses': return 'Open Source licenties'; - case 'about.versionLabel': return ({required Object version}) => 'Versie ${version}'; - case 'about.appDescription': return 'Een mooie Plex client voor Flutter'; - case 'about.viewLicensesDescription': return 'Bekijk licenties van third-party bibliotheken'; - case 'serverSelection.connectingToServer': return 'Verbinden met server...'; - case 'serverSelection.connectingToAllServers': return 'Verbinden met alle servers...'; - case 'serverSelection.connectToAllServers': return 'Verbind met alle servers'; - case 'serverSelection.allServerConnectionsFailed': return 'Kon niet verbinden met servers. Controleer je netwerk en probeer opnieuw.'; - case 'serverSelection.foundServers': return ({required Object count}) => '${count} server(s) gevonden'; - case 'serverSelection.orSelectIndividually': return 'Of selecteer een server individueel:'; - case 'serverSelection.serverDebugCopied': return 'Server debug gegevens gekopieerd naar klembord'; - case 'serverSelection.copyDebugData': return 'Kopieer debug gegevens'; - case 'serverSelection.noServersFound': return 'Geen servers gevonden'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => 'Geen servers gevonden voor ${username} (${email})'; - case 'serverSelection.malformedServerData': return ({required Object count}) => '${count} server(s) gevonden met verkeerde data. Geen geldige servers beschikbaar.'; - case 'serverSelection.incompleteServerInfo': return 'Sommige servers hebben incomplete informatie en zijn overgeslagen. Controleer je Plex.tv account.'; - case 'serverSelection.incompleteConnectionInfo': return 'Server verbinding informatie is incompleet. Probeer opnieuw.'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => 'Server informatie is verkeerd geformatteerd: ${message}'; - case 'serverSelection.networkConnectionFailed': return 'Netwerkverbinding mislukt. Controleer je internetverbinding en probeer opnieuw.'; - case 'serverSelection.authenticationFailed': return 'Authenticatie mislukt. Log opnieuw in.'; - case 'serverSelection.plexServiceUnavailable': return 'Plex service niet beschikbaar. Probeer later opnieuw.'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => 'Kon servers niet laden: ${error}'; - case 'hubDetail.title': return 'Titel'; - case 'hubDetail.releaseYear': return 'Uitgavejaar'; - case 'hubDetail.dateAdded': return 'Datum toegevoegd'; - case 'hubDetail.rating': return 'Beoordeling'; - case 'hubDetail.noItemsFound': return 'Geen items gevonden'; - case 'logs.title': return 'Logbestanden'; - case 'logs.clearLogs': return 'Wis logs'; - case 'logs.copyLogs': return 'Kopieer logs'; - case 'logs.exportLogs': return 'Exporteer logs'; - case 'logs.noLogsToShow': return 'Geen logs om te tonen'; - case 'logs.error': return 'Fout:'; - case 'logs.stackTrace': return 'Stacktracering:'; - case 'licenses.relatedPackages': return 'Gerelateerde pakketten'; - case 'licenses.license': return 'Licentie'; - case 'licenses.licenseNumber': return ({required Object number}) => 'Licentie ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} licenties'; - case 'navigation.home': return 'Thuis'; - case 'navigation.search': return 'Zoeken'; - case 'navigation.libraries': return 'Bibliotheken'; - case 'navigation.settings': return 'Instellingen'; - case 'playlists.title': return 'Afspeellijsten'; - case 'playlists.noPlaylists': return 'Geen afspeellijsten gevonden'; - case 'playlists.create': return 'Afspeellijst maken'; - case 'playlists.newPlaylist': return 'Nieuwe afspeellijst'; - case 'playlists.playlistName': return 'Naam afspeellijst'; - case 'playlists.enterPlaylistName': return 'Voer naam afspeellijst in'; - case 'playlists.edit': return 'Afspeellijst bewerken'; - case 'playlists.delete': return 'Afspeellijst verwijderen'; - case 'playlists.addTo': return 'Toevoegen aan afspeellijst'; - case 'playlists.addItems': return 'Items toevoegen'; - case 'playlists.removeItem': return 'Verwijderen uit afspeellijst'; - case 'playlists.clearPlaylist': return 'Afspeellijst wissen'; - case 'playlists.playAll': return 'Alles afspelen'; - case 'playlists.shuffle': return 'Willekeurig'; - case 'playlists.smartPlaylist': return 'Slimme afspeellijst'; - case 'playlists.regularPlaylist': return 'Normale afspeellijst'; - case 'playlists.itemCount': return ({required Object count}) => '${count} items'; - case 'playlists.oneItem': return '1 item'; - case 'playlists.emptyPlaylist': return 'Deze afspeellijst is leeg'; - case 'playlists.deleteConfirm': return 'Afspeellijst verwijderen?'; - case 'playlists.deleteMessage': return ({required Object name}) => 'Weet je zeker dat je "${name}" wilt verwijderen?'; - case 'playlists.created': return 'Afspeellijst gemaakt'; - case 'playlists.updated': return 'Afspeellijst bijgewerkt'; - case 'playlists.deleted': return 'Afspeellijst verwijderd'; - case 'playlists.itemAdded': return 'Toegevoegd aan afspeellijst'; - case 'playlists.itemRemoved': return 'Verwijderd uit afspeellijst'; - case 'playlists.selectPlaylist': return 'Selecteer afspeellijst'; - case 'playlists.createNewPlaylist': return 'Nieuwe afspeellijst maken'; - case 'playlists.errorCreating': return 'Fout bij maken afspeellijst'; - case 'playlists.errorDeleting': return 'Fout bij verwijderen afspeellijst'; - case 'playlists.errorLoading': return 'Fout bij laden afspeellijsten'; - case 'playlists.errorAdding': return 'Fout bij toevoegen aan afspeellijst'; - case 'playlists.errorReordering': return 'Fout bij herschikken van afspeellijstitem'; - case 'playlists.errorRemoving': return 'Fout bij verwijderen uit afspeellijst'; - case 'playlists.playlist': return 'Afspeellijst'; - case 'collections.title': return 'Collecties'; - case 'collections.collection': return 'Collectie'; - case 'collections.empty': return 'Collectie is leeg'; - case 'collections.noItems': return 'Geen items in deze collectie'; - case 'collections.unknownLibrarySection': return 'Kan niet verwijderen: onbekende bibliotheeksectie'; - case 'collections.deleteCollection': return 'Collectie verwijderen'; - case 'collections.deleteConfirm': return ({required Object title}) => 'Weet je zeker dat je "${title}" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.'; - case 'collections.deleted': return 'Collectie verwijderd'; - case 'collections.deleteFailed': return 'Collectie verwijderen mislukt'; - case 'collections.deleteFailedWithError': return ({required Object error}) => 'Collectie verwijderen mislukt: ${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => 'Collectie-items laden mislukt: ${error}'; - case 'collections.addTo': return 'Toevoegen aan collectie'; - case 'collections.selectCollection': return 'Selecteer collectie'; - case 'collections.createNewCollection': return 'Nieuwe collectie maken'; - case 'collections.collectionName': return 'Collectienaam'; - case 'collections.enterCollectionName': return 'Voer collectienaam in'; - case 'collections.addedToCollection': return 'Toegevoegd aan collectie'; - case 'collections.errorAddingToCollection': return 'Fout bij toevoegen aan collectie'; - case 'collections.created': return 'Collectie gemaakt'; - case 'collections.removeFromCollection': return 'Verwijderen uit collectie'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => '"${title}" uit deze collectie verwijderen?'; - case 'collections.removedFromCollection': return 'Uit collectie verwijderd'; - case 'collections.removeFromCollectionFailed': return 'Verwijderen uit collectie mislukt'; - case 'collections.removeFromCollectionError': return ({required Object error}) => 'Fout bij verwijderen uit collectie: ${error}'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return 'Laden...'; + case 'auth.signInWithPlex': + return 'Inloggen met Plex'; + case 'auth.showQRCode': + return 'Toon QR-code'; + case 'auth.cancel': + return 'Annuleren'; + case 'auth.authenticate': + return 'Authenticeren'; + case 'auth.retry': + return 'Opnieuw proberen'; + case 'auth.debugEnterToken': + return 'Debug: Voer Plex Token in'; + case 'auth.plexTokenLabel': + return 'Plex Authenticatietoken'; + case 'auth.plexTokenHint': + return 'Voer je Plex.tv token in'; + case 'auth.authenticationTimeout': + return 'Authenticatie verlopen. Probeer opnieuw.'; + case 'auth.scanQRCodeInstruction': + return 'Scan deze QR-code met een apparaat dat is ingelogd op Plex om te authenticeren.'; + case 'auth.waitingForAuth': + return 'Wachten op authenticatie...\nVoltooi het inloggen in je browser.'; + case 'common.cancel': + return 'Annuleren'; + case 'common.save': + return 'Opslaan'; + case 'common.close': + return 'Sluiten'; + case 'common.clear': + return 'Wissen'; + case 'common.reset': + return 'Resetten'; + case 'common.later': + return 'Later'; + case 'common.submit': + return 'Verzenden'; + case 'common.confirm': + return 'Bevestigen'; + case 'common.retry': + return 'Opnieuw proberen'; + case 'common.playNow': + return 'Nu afspelen'; + case 'common.logout': + return 'Uitloggen'; + case 'common.online': + return 'Online'; + case 'common.offline': + return 'Offline'; + case 'common.owned': + return 'Eigendom'; + case 'common.shared': + return 'Gedeeld'; + case 'common.current': + return 'HUIDIG'; + case 'common.unknown': + return 'Onbekend'; + case 'common.refresh': + return 'Vernieuwen'; + case 'common.yes': + return 'Ja'; + case 'common.no': + return 'Nee'; + case 'common.server': + return 'Server'; + case 'common.delete': + return 'Verwijderen'; + case 'common.shuffle': + return 'Willekeurig'; + case 'common.addTo': + return 'Toevoegen aan...'; + case 'screens.licenses': + return 'Licenties'; + case 'screens.selectServer': + return 'Selecteer server'; + case 'screens.switchProfile': + return 'Wissel van profiel'; + case 'screens.subtitleStyling': + return 'Ondertitel opmaak'; + case 'screens.search': + return 'Zoeken'; + case 'screens.logs': + return 'Logbestanden'; + case 'update.available': + return 'Update beschikbaar'; + case 'update.versionAvailable': + return ({required Object version}) => + 'Versie ${version} is beschikbaar'; + case 'update.currentVersion': + return ({required Object version}) => 'Huidig: ${version}'; + case 'update.skipVersion': + return 'Deze versie overslaan'; + case 'update.viewRelease': + return 'Bekijk release'; + case 'update.latestVersion': + return 'Je hebt de nieuwste versie'; + case 'update.checkFailed': + return 'Kon niet controleren op updates'; + case 'settings.title': + return 'Instellingen'; + case 'settings.language': + return 'Taal'; + case 'settings.theme': + return 'Thema'; + case 'settings.appearance': + return 'Uiterlijk'; + case 'settings.videoPlayback': + return 'Video afspelen'; + case 'settings.shufflePlay': + return 'Willekeurig afspelen'; + case 'settings.advanced': + return 'Geavanceerd'; + case 'settings.useSeasonPostersDescription': + return 'Toon seizoenposter in plaats van serieposter voor afleveringen'; + case 'settings.showHeroSectionDescription': + return 'Toon uitgelichte inhoud carrousel op startscherm'; + case 'settings.secondsLabel': + return 'Seconden'; + case 'settings.minutesLabel': + return 'Minuten'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + 'Voer duur in (${min}-${max})'; + case 'settings.systemTheme': + return 'Systeem'; + case 'settings.systemThemeDescription': + return 'Volg systeeminstellingen'; + case 'settings.lightTheme': + return 'Licht'; + case 'settings.darkTheme': + return 'Donker'; + case 'settings.libraryDensity': + return 'Bibliotheek dichtheid'; + case 'settings.compact': + return 'Compact'; + case 'settings.compactDescription': + return 'Kleinere kaarten, meer items zichtbaar'; + case 'settings.normal': + return 'Normaal'; + case 'settings.normalDescription': + return 'Standaard grootte'; + case 'settings.comfortable': + return 'Comfortabel'; + case 'settings.comfortableDescription': + return 'Grotere kaarten, minder items zichtbaar'; + case 'settings.viewMode': + return 'Weergavemodus'; + case 'settings.gridView': + return 'Raster'; + case 'settings.gridViewDescription': + return 'Items weergeven in een rasterindeling'; + case 'settings.listView': + return 'Lijst'; + case 'settings.listViewDescription': + return 'Items weergeven in een lijstindeling'; + case 'settings.useSeasonPosters': + return 'Gebruik seizoenposters'; + case 'settings.showHeroSection': + return 'Toon hoofdsectie'; + case 'settings.hardwareDecoding': + return 'Hardware decodering'; + case 'settings.hardwareDecodingDescription': + return 'Gebruik hardware versnelling indien beschikbaar'; + case 'settings.bufferSize': + return 'Buffer grootte'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return 'Ondertitel opmaak'; + case 'settings.subtitleStylingDescription': + return 'Pas ondertitel uiterlijk aan'; + case 'settings.smallSkipDuration': + return 'Korte skip duur'; + case 'settings.largeSkipDuration': + return 'Lange skip duur'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} seconden'; + case 'settings.defaultSleepTimer': + return 'Standaard slaap timer'; + case 'settings.minutesUnit': + return ({required Object minutes}) => 'bij ${minutes} minuten'; + case 'settings.rememberTrackSelections': + return 'Onthoud track selecties per serie/film'; + case 'settings.rememberTrackSelectionsDescription': + return 'Bewaar automatisch audio- en ondertiteltaalvoorkeuren wanneer je tracks wijzigt tijdens afspelen'; + case 'settings.unwatchedOnly': + return 'Alleen ongekeken'; + case 'settings.unwatchedOnlyDescription': + return 'Alleen ongekeken afleveringen opnemen in willekeurige wachtrij'; + case 'settings.shuffleOrderNavigation': + return 'Willekeurige volgorde navigatie'; + case 'settings.shuffleOrderNavigationDescription': + return 'Volgende/vorige knoppen volgen willekeurige volgorde'; + case 'settings.loopShuffleQueue': + return 'Herhaal willekeurige wachtrij'; + case 'settings.loopShuffleQueueDescription': + return 'Start wachtrij opnieuw bij het einde'; + case 'settings.videoPlayerControls': + return 'Videospeler bediening'; + case 'settings.keyboardShortcuts': + return 'Toetsenbord sneltoetsen'; + case 'settings.keyboardShortcutsDescription': + return 'Pas toetsenbord sneltoetsen aan'; + case 'settings.debugLogging': + return 'Debug logging'; + case 'settings.debugLoggingDescription': + return 'Schakel gedetailleerde logging in voor probleemoplossing'; + case 'settings.viewLogs': + return 'Bekijk logs'; + case 'settings.viewLogsDescription': + return 'Bekijk applicatie logs'; + case 'settings.clearCache': + return 'Cache wissen'; + case 'settings.clearCacheDescription': + return 'Dit wist alle gecachte afbeeldingen en gegevens. De app kan langer duren om inhoud te laden na het wissen van de cache.'; + case 'settings.clearCacheSuccess': + return 'Cache succesvol gewist'; + case 'settings.resetSettings': + return 'Instellingen resetten'; + case 'settings.resetSettingsDescription': + return 'Dit reset alle instellingen naar hun standaard waarden. Deze actie kan niet ongedaan gemaakt worden.'; + case 'settings.resetSettingsSuccess': + return 'Instellingen succesvol gereset'; + case 'settings.shortcutsReset': + return 'Sneltoetsen gereset naar standaard'; + case 'settings.about': + return 'Over'; + case 'settings.aboutDescription': + return 'App informatie en licenties'; + case 'settings.updates': + return 'Updates'; + case 'settings.updateAvailable': + return 'Update beschikbaar'; + case 'settings.checkForUpdates': + return 'Controleer op updates'; + case 'settings.validationErrorEnterNumber': + return 'Voer een geldig nummer in'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => 'Duur moet tussen ${min} en ${max} ${unit} zijn'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => + 'Sneltoets al toegewezen aan ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => + 'Sneltoets bijgewerkt voor ${action}'; + case 'settings.autoSkip': + return 'Automatisch Overslaan'; + case 'settings.autoSkipIntro': + return 'Intro Automatisch Overslaan'; + case 'settings.autoSkipIntroDescription': + return 'Intro-markeringen na enkele seconden automatisch overslaan'; + case 'settings.autoSkipCredits': + return 'Credits Automatisch Overslaan'; + case 'settings.autoSkipCreditsDescription': + return 'Credits automatisch overslaan en volgende aflevering afspelen'; + case 'settings.autoSkipDelay': + return 'Vertraging Automatisch Overslaan'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => + '${seconds} seconden wachten voor automatisch overslaan'; + case 'settings.downloads': + return 'Downloads'; + case 'settings.downloadLocationDescription': + return 'Kies waar gedownloade content wordt opgeslagen'; + case 'settings.downloadLocationDefault': + return 'Standaard (App-opslag)'; + case 'settings.downloadLocationCustom': + return 'Aangepaste Locatie'; + case 'settings.selectFolder': + return 'Selecteer Map'; + case 'settings.resetToDefault': + return 'Herstel naar Standaard'; + case 'settings.currentPath': + return ({required Object path}) => 'Huidig: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'Aangepaste downloadlocatie wordt niet ondersteund op iOS'; + case 'settings.downloadLocationChanged': + return 'Downloadlocatie gewijzigd'; + case 'settings.downloadLocationReset': + return 'Downloadlocatie hersteld naar standaard'; + case 'settings.downloadLocationInvalid': + return 'Geselecteerde map is niet beschrijfbaar'; + case 'settings.downloadLocationSelectError': + return 'Kan map niet selecteren'; + case 'settings.downloadOnWifiOnly': + return 'Alleen via WiFi downloaden'; + case 'settings.downloadOnWifiOnlyDescription': + return 'Voorkom downloads bij gebruik van mobiele data'; + case 'settings.cellularDownloadBlocked': + return 'Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.'; + case 'search.hint': + return 'Zoek films, series, muziek...'; + case 'search.tryDifferentTerm': + return 'Probeer een andere zoekterm'; + case 'search.searchYourMedia': + return 'Zoek in je media'; + case 'search.enterTitleActorOrKeyword': + return 'Voer een titel, acteur of trefwoord in'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => + 'Stel sneltoets in voor ${actionName}'; + case 'hotkeys.clearShortcut': + return 'Wis sneltoets'; + case 'pinEntry.enterPin': + return 'Voer PIN in'; + case 'pinEntry.showPin': + return 'Toon PIN'; + case 'pinEntry.hidePin': + return 'Verberg PIN'; + case 'fileInfo.title': + return 'Bestand info'; + case 'fileInfo.video': + return 'Video'; + case 'fileInfo.audio': + return 'Audio'; + case 'fileInfo.file': + return 'Bestand'; + case 'fileInfo.advanced': + return 'Geavanceerd'; + case 'fileInfo.codec': + return 'Codec'; + case 'fileInfo.resolution': + return 'Resolutie'; + case 'fileInfo.bitrate': + return 'Bitrate'; + case 'fileInfo.frameRate': + return 'Frame rate'; + case 'fileInfo.aspectRatio': + return 'Beeldverhouding'; + case 'fileInfo.profile': + return 'Profiel'; + case 'fileInfo.bitDepth': + return 'Bit diepte'; + case 'fileInfo.colorSpace': + return 'Kleurruimte'; + case 'fileInfo.colorRange': + return 'Kleurbereik'; + case 'fileInfo.colorPrimaries': + return 'Kleurprimaires'; + case 'fileInfo.chromaSubsampling': + return 'Chroma subsampling'; + case 'fileInfo.channels': + return 'Kanalen'; + case 'fileInfo.path': + return 'Pad'; + case 'fileInfo.size': + return 'Grootte'; + case 'fileInfo.container': + return 'Container'; + case 'fileInfo.duration': + return 'Duur'; + case 'fileInfo.optimizedForStreaming': + return 'Geoptimaliseerd voor streaming'; + case 'fileInfo.has64bitOffsets': + return '64-bit Offsets'; + case 'mediaMenu.markAsWatched': + return 'Markeer als gekeken'; + case 'mediaMenu.markAsUnwatched': + return 'Markeer als ongekeken'; + case 'mediaMenu.removeFromContinueWatching': + return 'Verwijder uit Doorgaan met kijken'; + case 'mediaMenu.goToSeries': + return 'Ga naar serie'; + case 'mediaMenu.goToSeason': + return 'Ga naar seizoen'; + case 'mediaMenu.shufflePlay': + return 'Willekeurig afspelen'; + case 'mediaMenu.fileInfo': + return 'Bestand info'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, film'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, TV-serie'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return 'bekeken'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '${percent} procent bekeken'; + case 'accessibility.mediaCardUnwatched': + return 'niet bekeken'; + case 'accessibility.tapToPlay': + return 'Tik om af te spelen'; + case 'tooltips.shufflePlay': + return 'Willekeurig afspelen'; + case 'tooltips.markAsWatched': + return 'Markeer als gekeken'; + case 'tooltips.markAsUnwatched': + return 'Markeer als ongekeken'; + case 'videoControls.audioLabel': + return 'Audio'; + case 'videoControls.subtitlesLabel': + return 'Ondertitels'; + case 'videoControls.resetToZero': + return 'Reset naar 0ms'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} speelt later af'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} speelt eerder af'; + case 'videoControls.noOffset': + return 'Geen offset'; + case 'videoControls.letterbox': + return 'Letterbox'; + case 'videoControls.fillScreen': + return 'Vul scherm'; + case 'videoControls.stretch': + return 'Uitrekken'; + case 'videoControls.lockRotation': + return 'Vergrendel rotatie'; + case 'videoControls.unlockRotation': + return 'Ontgrendel rotatie'; + case 'videoControls.sleepTimer': + return 'Slaaptimer'; + case 'videoControls.timerActive': + return 'Timer actief'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => + 'Afspelen wordt gepauzeerd over ${duration}'; + case 'videoControls.sleepTimerCompleted': + return 'Slaaptimer voltooid - afspelen gepauzeerd'; + case 'videoControls.playButton': + return 'Afspelen'; + case 'videoControls.pauseButton': + return 'Pauzeren'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => + 'Terugspoelen ${seconds} seconden'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => + 'Vooruitspoelen ${seconds} seconden'; + case 'videoControls.previousButton': + return 'Vorige aflevering'; + case 'videoControls.nextButton': + return 'Volgende aflevering'; + case 'videoControls.previousChapterButton': + return 'Vorig hoofdstuk'; + case 'videoControls.nextChapterButton': + return 'Volgend hoofdstuk'; + case 'videoControls.volumeButton': + return 'Volume'; + case 'videoControls.muteButton': + return 'Dempen'; + case 'videoControls.unmuteButton': + return 'Dempen opheffen'; + case 'videoControls.settingsButton': + return 'Video-instellingen'; + case 'videoControls.audioTrackButton': + return 'Audiosporen'; + case 'videoControls.subtitlesButton': + return 'Ondertitels'; + case 'videoControls.chaptersButton': + return 'Hoofdstukken'; + case 'videoControls.versionsButton': + return 'Videoversies'; + case 'videoControls.aspectRatioButton': + return 'Beeldverhouding'; + case 'videoControls.fullscreenButton': + return 'Volledig scherm activeren'; + case 'videoControls.exitFullscreenButton': + return 'Volledig scherm verlaten'; + case 'videoControls.rotationLockButton': + return 'Rotatievergrendeling'; + case 'videoControls.timelineSlider': + return 'Videotijdlijn'; + case 'videoControls.volumeSlider': + return 'Volumeniveau'; + case 'videoControls.backButton': + return 'Terug'; + case 'userStatus.admin': + return 'Beheerder'; + case 'userStatus.restricted': + return 'Beperkt'; + case 'userStatus.protected': + return 'Beschermd'; + case 'userStatus.current': + return 'HUIDIG'; + case 'messages.markedAsWatched': + return 'Gemarkeerd als gekeken'; + case 'messages.markedAsUnwatched': + return 'Gemarkeerd als ongekeken'; + case 'messages.markedAsWatchedOffline': + return 'Gemarkeerd als gekeken (sync wanneer online)'; + case 'messages.markedAsUnwatchedOffline': + return 'Gemarkeerd als ongekeken (sync wanneer online)'; + case 'messages.removedFromContinueWatching': + return 'Verwijderd uit Doorgaan met kijken'; + case 'messages.errorLoading': + return ({required Object error}) => 'Fout: ${error}'; + case 'messages.fileInfoNotAvailable': + return 'Bestand informatie niet beschikbaar'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => + 'Fout bij laden bestand info: ${error}'; + case 'messages.errorLoadingSeries': + return 'Fout bij laden serie'; + case 'messages.errorLoadingSeason': + return 'Fout bij laden seizoen'; + case 'messages.musicNotSupported': + return 'Muziek afspelen wordt nog niet ondersteund'; + case 'messages.logsCleared': + return 'Logs gewist'; + case 'messages.logsCopied': + return 'Logs gekopieerd naar klembord'; + case 'messages.noLogsAvailable': + return 'Geen logs beschikbaar'; + case 'messages.libraryScanning': + return ({required Object title}) => 'Scannen "${title}"...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => + 'Bibliotheek scan gestart voor "${title}"'; + case 'messages.libraryScanFailed': + return ({required Object error}) => + 'Kon bibliotheek niet scannen: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => + 'Metadata vernieuwen voor "${title}"...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => + 'Metadata vernieuwen gestart voor "${title}"'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => + 'Kon metadata niet vernieuwen: ${error}'; + case 'messages.noPlexToken': + return 'Geen Plex token gevonden. Log opnieuw in.'; + case 'messages.logoutConfirm': + return 'Weet je zeker dat je wilt uitloggen?'; + case 'messages.noSeasonsFound': + return 'Geen seizoenen gevonden'; + case 'messages.noEpisodesFound': + return 'Geen afleveringen gevonden in eerste seizoen'; + case 'messages.noEpisodesFoundGeneral': + return 'Geen afleveringen gevonden'; + case 'messages.noResultsFound': + return 'Geen resultaten gevonden'; + case 'messages.sleepTimerSet': + return ({required Object label}) => + 'Slaap timer ingesteld voor ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => + 'Kon niet wisselen naar ${displayName}'; + case 'messages.noItemsAvailable': + return 'Geen items beschikbaar'; + case 'messages.failedToCreatePlayQueue': + return 'Kan afspeelwachtrij niet maken'; + case 'messages.failedToCreatePlayQueueNoItems': + return 'Kan afspeelwachtrij niet maken - geen items'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + 'Afspelen van ${action} mislukt: ${error}'; + case 'messages.youAreOffline': + return 'Je bent offline'; + case 'messages.offlineFeatureUnavailable': + return 'Deze functie vereist een internetverbinding. Je downloads en instellingen zijn nog steeds beschikbaar.'; + case 'profile.noUsersAvailable': + return 'Geen gebruikers beschikbaar'; + case 'subtitlingStyling.stylingOptions': + return 'Opmaak opties'; + case 'subtitlingStyling.fontSize': + return 'Lettergrootte'; + case 'subtitlingStyling.textColor': + return 'Tekstkleur'; + case 'subtitlingStyling.borderSize': + return 'Rand grootte'; + case 'subtitlingStyling.borderColor': + return 'Randkleur'; + case 'subtitlingStyling.backgroundOpacity': + return 'Achtergrond transparantie'; + case 'subtitlingStyling.backgroundColor': + return 'Achtergrondkleur'; + case 'dialog.confirmAction': + return 'Bevestig actie'; + case 'dialog.areYouSure': + return 'Weet je zeker dat je deze actie wilt uitvoeren?'; + case 'dialog.cancel': + return 'Annuleren'; + case 'dialog.playNow': + return 'Nu afspelen'; + case 'discover.title': + return 'Ontdekken'; + case 'discover.switchProfile': + return 'Wissel van profiel'; + case 'discover.switchServer': + return 'Wissel van server'; + case 'discover.logout': + return 'Uitloggen'; + case 'discover.noContentAvailable': + return 'Geen inhoud beschikbaar'; + case 'discover.addMediaToLibraries': + return 'Voeg wat media toe aan je bibliotheken'; + case 'discover.continueWatching': + return 'Verder kijken'; + case 'discover.recentlyAdded': + return 'Recent toegevoegd'; + case 'discover.play': + return 'Afspelen'; + case 'discover.resume': + return 'Hervatten'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return 'Pauzeren'; + case 'discover.overview': + return 'Overzicht'; + case 'discover.cast': + return 'Acteurs'; + case 'discover.seasons': + return 'Seizoenen'; + case 'discover.studio': + return 'Studio'; + case 'discover.rating': + return 'Leeftijd'; + case 'discover.watched': + return 'Bekeken'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} afleveringen'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '${watched}/${total} gekeken'; + case 'discover.movie': + return 'Film'; + case 'discover.tvShow': + return 'TV Serie'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '${minutes} min over'; + case 'errors.searchFailed': + return ({required Object error}) => 'Zoeken mislukt: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => + 'Verbinding time-out tijdens laden ${context}'; + case 'errors.connectionFailed': + return 'Kan geen verbinding maken met Plex server'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + 'Kon ${context} niet laden: ${error}'; + case 'errors.noClientAvailable': + return 'Geen client beschikbaar'; + case 'errors.authenticationFailed': + return ({required Object error}) => 'Authenticatie mislukt: ${error}'; + case 'errors.couldNotLaunchUrl': + return 'Kon auth URL niet openen'; + case 'errors.pleaseEnterToken': + return 'Voer een token in'; + case 'errors.invalidToken': + return 'Ongeldig token'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => + 'Kon token niet verifiëren: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => + 'Kon niet wisselen naar ${displayName}'; + case 'errors.connectionFailedGeneric': + return 'Verbinding mislukt'; + case 'libraries.title': + return 'Bibliotheken'; + case 'libraries.scanLibraryFiles': + return 'Scan bibliotheek bestanden'; + case 'libraries.scanLibrary': + return 'Scan bibliotheek'; + case 'libraries.analyze': + return 'Analyseren'; + case 'libraries.analyzeLibrary': + return 'Analyseer bibliotheek'; + case 'libraries.refreshMetadata': + return 'Vernieuw metadata'; + case 'libraries.emptyTrash': + return 'Prullenbak legen'; + case 'libraries.emptyingTrash': + return ({required Object title}) => + 'Prullenbak legen voor "${title}"...'; + case 'libraries.trashEmptied': + return ({required Object title}) => + 'Prullenbak geleegd voor "${title}"'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => + 'Kon prullenbak niet legen: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => 'Analyseren "${title}"...'; + case 'libraries.analysisStarted': + return ({required Object title}) => 'Analyse gestart voor "${title}"'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => + 'Kon bibliotheek niet analyseren: ${error}'; + case 'libraries.noLibrariesFound': + return 'Geen bibliotheken gevonden'; + case 'libraries.thisLibraryIsEmpty': + return 'Deze bibliotheek is leeg'; + case 'libraries.all': + return 'Alles'; + case 'libraries.clearAll': + return 'Alles wissen'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => + 'Weet je zeker dat je "${title}" wilt scannen?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => + 'Weet je zeker dat je "${title}" wilt analyseren?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => + 'Weet je zeker dat je metadata wilt vernieuwen voor "${title}"?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => + 'Weet je zeker dat je de prullenbak wilt legen voor "${title}"?'; + case 'libraries.manageLibraries': + return 'Beheer bibliotheken'; + case 'libraries.sort': + return 'Sorteren'; + case 'libraries.sortBy': + return 'Sorteer op'; + case 'libraries.filters': + return 'Filters'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => + 'Bibliotheek laden... (${count} items geladen)'; + case 'libraries.confirmActionMessage': + return 'Weet je zeker dat je deze actie wilt uitvoeren?'; + case 'libraries.showLibrary': + return 'Toon bibliotheek'; + case 'libraries.hideLibrary': + return 'Verberg bibliotheek'; + case 'libraries.libraryOptions': + return 'Bibliotheek opties'; + case 'libraries.content': + return 'bibliotheekinhoud'; + case 'libraries.selectLibrary': + return 'Bibliotheek kiezen'; + case 'libraries.filtersWithCount': + return ({required Object count}) => 'Filters (${count})'; + case 'libraries.noRecommendations': + return 'Geen aanbevelingen beschikbaar'; + case 'libraries.noCollections': + return 'Geen collecties in deze bibliotheek'; + case 'libraries.noFoldersFound': + return 'Geen mappen gevonden'; + case 'libraries.folders': + return 'mappen'; + case 'libraries.tabs.recommended': + return 'Aanbevolen'; + case 'libraries.tabs.browse': + return 'Bladeren'; + case 'libraries.tabs.collections': + return 'Collecties'; + case 'libraries.tabs.playlists': + return 'Afspeellijsten'; + case 'libraries.groupings.all': + return 'Alles'; + case 'libraries.groupings.movies': + return 'Films'; + case 'libraries.groupings.shows': + return 'Series'; + case 'libraries.groupings.seasons': + return 'Seizoenen'; + case 'libraries.groupings.episodes': + return 'Afleveringen'; + case 'libraries.groupings.folders': + return 'Mappen'; + case 'about.title': + return 'Over'; + case 'about.openSourceLicenses': + return 'Open Source licenties'; + case 'about.versionLabel': + return ({required Object version}) => 'Versie ${version}'; + case 'about.appDescription': + return 'Een mooie Plex client voor Flutter'; + case 'about.viewLicensesDescription': + return 'Bekijk licenties van third-party bibliotheken'; + case 'serverSelection.connectingToServer': + return 'Verbinden met server...'; + case 'serverSelection.connectingToAllServers': + return 'Verbinden met alle servers...'; + case 'serverSelection.connectToAllServers': + return 'Verbind met alle servers'; + case 'serverSelection.allServerConnectionsFailed': + return 'Kon niet verbinden met servers. Controleer je netwerk en probeer opnieuw.'; + case 'serverSelection.foundServers': + return ({required Object count}) => '${count} server(s) gevonden'; + case 'serverSelection.orSelectIndividually': + return 'Of selecteer een server individueel:'; + case 'serverSelection.serverDebugCopied': + return 'Server debug gegevens gekopieerd naar klembord'; + case 'serverSelection.copyDebugData': + return 'Kopieer debug gegevens'; + case 'serverSelection.noServersFound': + return 'Geen servers gevonden'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + 'Geen servers gevonden voor ${username} (${email})'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + '${count} server(s) gevonden met verkeerde data. Geen geldige servers beschikbaar.'; + case 'serverSelection.incompleteServerInfo': + return 'Sommige servers hebben incomplete informatie en zijn overgeslagen. Controleer je Plex.tv account.'; + case 'serverSelection.incompleteConnectionInfo': + return 'Server verbinding informatie is incompleet. Probeer opnieuw.'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => + 'Server informatie is verkeerd geformatteerd: ${message}'; + case 'serverSelection.networkConnectionFailed': + return 'Netwerkverbinding mislukt. Controleer je internetverbinding en probeer opnieuw.'; + case 'serverSelection.authenticationFailed': + return 'Authenticatie mislukt. Log opnieuw in.'; + case 'serverSelection.plexServiceUnavailable': + return 'Plex service niet beschikbaar. Probeer later opnieuw.'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => 'Kon servers niet laden: ${error}'; + case 'hubDetail.title': + return 'Titel'; + case 'hubDetail.releaseYear': + return 'Uitgavejaar'; + case 'hubDetail.dateAdded': + return 'Datum toegevoegd'; + case 'hubDetail.rating': + return 'Beoordeling'; + case 'hubDetail.noItemsFound': + return 'Geen items gevonden'; + case 'logs.title': + return 'Logbestanden'; + case 'logs.clearLogs': + return 'Wis logs'; + case 'logs.copyLogs': + return 'Kopieer logs'; + case 'logs.exportLogs': + return 'Exporteer logs'; + case 'logs.noLogsToShow': + return 'Geen logs om te tonen'; + case 'logs.error': + return 'Fout:'; + case 'logs.stackTrace': + return 'Stacktracering:'; + case 'licenses.relatedPackages': + return 'Gerelateerde pakketten'; + case 'licenses.license': + return 'Licentie'; + case 'licenses.licenseNumber': + return ({required Object number}) => 'Licentie ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} licenties'; + case 'navigation.home': + return 'Thuis'; + case 'navigation.search': + return 'Zoeken'; + case 'navigation.libraries': + return 'Bibliotheken'; + case 'navigation.settings': + return 'Instellingen'; + case 'navigation.downloads': + return 'Downloads'; + case 'downloads.title': + return 'Downloads'; + case 'downloads.manage': + return 'Beheren'; + case 'downloads.tvShows': + return 'Series'; + case 'downloads.movies': + return 'Films'; + case 'downloads.queue': + return 'Wachtrij'; + case 'downloads.noDownloads': + return 'Nog geen downloads'; + case 'downloads.noDownloadsDescription': + return 'Gedownloade content verschijnt hier voor offline weergave'; + case 'downloads.browseContent': + return 'Blader door content'; + case 'downloads.downloading': + return 'Downloaden'; + case 'downloads.downloaded': + return 'Gedownload'; + case 'downloads.paused': + return 'Gepauzeerd'; + case 'downloads.failed': + return 'Mislukt'; + case 'downloads.queued': + return 'In wachtrij'; + case 'downloads.downloadNow': + return 'Download'; + case 'downloads.deleteDownload': + return 'Download verwijderen'; + case 'downloads.cancelDownload': + return 'Download annuleren'; + case 'downloads.pauseDownload': + return 'Download pauzeren'; + case 'downloads.resumeDownload': + return 'Download hervatten'; + case 'downloads.retryDownload': + return 'Download opnieuw proberen'; + case 'downloads.downloadComplete': + return 'Download voltooid'; + case 'downloads.downloadFailed': + return 'Download mislukt'; + case 'downloads.downloadCancelled': + return 'Download geannuleerd'; + case 'downloads.downloadQueued': + return 'Download in wachtrij'; + case 'downloads.episodesQueued': + return ({required Object count}) => + '${count} afleveringen in wachtrij voor download'; + case 'downloads.downloadDeleted': + return 'Download verwijderd'; + case 'downloads.deleteConfirmTitle': + return 'Download verwijderen?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + 'Weet je zeker dat je "${title}" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.'; + case 'downloads.deleteConfirmMessage': + return 'Het gedownloade bestand wordt van je apparaat verwijderd.'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => 'Verwijderen van ${title}... (${current} van ${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => 'Verwijderen van ${title}...'; + case 'downloads.storageUsed': + return 'Opslag gebruikt'; + case 'downloads.availableOffline': + return 'Offline beschikbaar'; + case 'downloads.downloadingSeason': + return 'Seizoen downloaden'; + case 'downloads.downloadingEpisode': + return 'Aflevering downloaden'; + case 'offline.youAreOffline': + return 'Je bent offline'; + case 'offline.offlineMode': + return 'Offline modus'; + case 'offline.backOnline': + return 'Weer online'; + case 'offline.showingDownloadedContent': + return 'Alleen gedownloade content wordt getoond'; + case 'offline.noInternetConnection': + return 'Geen internetverbinding'; + case 'offline.serversUnreachable': + return 'Plex servers onbereikbaar'; + case 'offline.connectToDownload': + return 'Maak verbinding met internet om content te downloaden'; + case 'playlists.title': + return 'Afspeellijsten'; + case 'playlists.noPlaylists': + return 'Geen afspeellijsten gevonden'; + case 'playlists.create': + return 'Afspeellijst maken'; + case 'playlists.newPlaylist': + return 'Nieuwe afspeellijst'; + case 'playlists.playlistName': + return 'Naam afspeellijst'; + case 'playlists.enterPlaylistName': + return 'Voer naam afspeellijst in'; + case 'playlists.edit': + return 'Afspeellijst bewerken'; + case 'playlists.delete': + return 'Afspeellijst verwijderen'; + case 'playlists.addTo': + return 'Toevoegen aan afspeellijst'; + case 'playlists.addItems': + return 'Items toevoegen'; + case 'playlists.removeItem': + return 'Verwijderen uit afspeellijst'; + case 'playlists.clearPlaylist': + return 'Afspeellijst wissen'; + case 'playlists.playAll': + return 'Alles afspelen'; + case 'playlists.shuffle': + return 'Willekeurig'; + case 'playlists.smartPlaylist': + return 'Slimme afspeellijst'; + case 'playlists.regularPlaylist': + return 'Normale afspeellijst'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} items'; + case 'playlists.oneItem': + return '1 item'; + case 'playlists.emptyPlaylist': + return 'Deze afspeellijst is leeg'; + case 'playlists.deleteConfirm': + return 'Afspeellijst verwijderen?'; + case 'playlists.deleteMessage': + return ({required Object name}) => + 'Weet je zeker dat je "${name}" wilt verwijderen?'; + case 'playlists.created': + return 'Afspeellijst gemaakt'; + case 'playlists.updated': + return 'Afspeellijst bijgewerkt'; + case 'playlists.deleted': + return 'Afspeellijst verwijderd'; + case 'playlists.itemAdded': + return 'Toegevoegd aan afspeellijst'; + case 'playlists.itemRemoved': + return 'Verwijderd uit afspeellijst'; + case 'playlists.selectPlaylist': + return 'Selecteer afspeellijst'; + case 'playlists.createNewPlaylist': + return 'Nieuwe afspeellijst maken'; + case 'playlists.errorCreating': + return 'Fout bij maken afspeellijst'; + case 'playlists.errorDeleting': + return 'Fout bij verwijderen afspeellijst'; + case 'playlists.errorLoading': + return 'Fout bij laden afspeellijsten'; + case 'playlists.errorAdding': + return 'Fout bij toevoegen aan afspeellijst'; + case 'playlists.errorReordering': + return 'Fout bij herschikken van afspeellijstitem'; + case 'playlists.errorRemoving': + return 'Fout bij verwijderen uit afspeellijst'; + case 'playlists.playlist': + return 'Afspeellijst'; + case 'collections.title': + return 'Collecties'; + case 'collections.collection': + return 'Collectie'; + case 'collections.empty': + return 'Collectie is leeg'; + case 'collections.noItems': + return 'Geen items in deze collectie'; + case 'collections.unknownLibrarySection': + return 'Kan niet verwijderen: onbekende bibliotheeksectie'; + case 'collections.deleteCollection': + return 'Collectie verwijderen'; + case 'collections.deleteConfirm': + return ({required Object title}) => + 'Weet je zeker dat je "${title}" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.'; + case 'collections.deleted': + return 'Collectie verwijderd'; + case 'collections.deleteFailed': + return 'Collectie verwijderen mislukt'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => + 'Collectie verwijderen mislukt: ${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => + 'Collectie-items laden mislukt: ${error}'; + case 'collections.addTo': + return 'Toevoegen aan collectie'; + case 'collections.selectCollection': + return 'Selecteer collectie'; + case 'collections.createNewCollection': + return 'Nieuwe collectie maken'; + case 'collections.collectionName': + return 'Collectienaam'; + case 'collections.enterCollectionName': + return 'Voer collectienaam in'; + case 'collections.addedToCollection': + return 'Toegevoegd aan collectie'; + case 'collections.errorAddingToCollection': + return 'Fout bij toevoegen aan collectie'; + case 'collections.created': + return 'Collectie gemaakt'; + case 'collections.removeFromCollection': + return 'Verwijderen uit collectie'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => + '"${title}" uit deze collectie verwijderen?'; + case 'collections.removedFromCollection': + return 'Uit collectie verwijderd'; + case 'collections.removeFromCollectionFailed': + return 'Verwijderen uit collectie mislukt'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => + 'Fout bij verwijderen uit collectie: ${error}'; + default: + return null; + } + } } extension on _StringsSv { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return 'Laddar...'; - case 'auth.signInWithPlex': return 'Logga in med Plex'; - case 'auth.showQRCode': return 'Visa QR-kod'; - case 'auth.cancel': return 'Avbryt'; - case 'auth.authenticate': return 'Autentisera'; - case 'auth.retry': return 'Försök igen'; - case 'auth.debugEnterToken': return 'Debug: Ange Plex-token'; - case 'auth.plexTokenLabel': return 'Plex-autentiseringstoken'; - case 'auth.plexTokenHint': return 'Ange din Plex.tv-token'; - case 'auth.authenticationTimeout': return 'Autentisering tog för lång tid. Försök igen.'; - case 'auth.scanQRCodeInstruction': return 'Skanna denna QR-kod med en enhet inloggad på Plex för att autentisera.'; - case 'auth.waitingForAuth': return 'Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.'; - case 'common.cancel': return 'Avbryt'; - case 'common.save': return 'Spara'; - case 'common.close': return 'Stäng'; - case 'common.clear': return 'Rensa'; - case 'common.reset': return 'Återställ'; - case 'common.later': return 'Senare'; - case 'common.submit': return 'Skicka'; - case 'common.confirm': return 'Bekräfta'; - case 'common.retry': return 'Försök igen'; - case 'common.playNow': return 'Spela nu'; - case 'common.logout': return 'Logga ut'; - case 'common.online': return 'Online'; - case 'common.offline': return 'Offline'; - case 'common.owned': return 'Egen'; - case 'common.shared': return 'Delad'; - case 'common.current': return 'NUVARANDE'; - case 'common.unknown': return 'Okänd'; - case 'common.refresh': return 'Uppdatera'; - case 'common.yes': return 'Ja'; - case 'common.no': return 'Nej'; - case 'common.server': return 'Server'; - case 'common.delete': return 'Ta bort'; - case 'common.shuffle': return 'Blanda'; - case 'common.addTo': return 'Lägg till i...'; - case 'screens.licenses': return 'Licenser'; - case 'screens.selectServer': return 'Välj server'; - case 'screens.switchProfile': return 'Byt profil'; - case 'screens.subtitleStyling': return 'Undertext-styling'; - case 'screens.search': return 'Sök'; - case 'screens.logs': return 'Loggar'; - case 'update.available': return 'Uppdatering tillgänglig'; - case 'update.versionAvailable': return ({required Object version}) => 'Version ${version} är tillgänglig'; - case 'update.currentVersion': return ({required Object version}) => 'Nuvarande: ${version}'; - case 'update.skipVersion': return 'Hoppa över denna version'; - case 'update.viewRelease': return 'Visa release'; - case 'update.latestVersion': return 'Du har den senaste versionen'; - case 'update.checkFailed': return 'Misslyckades att kontrollera uppdateringar'; - case 'settings.title': return 'Inställningar'; - case 'settings.language': return 'Språk'; - case 'settings.theme': return 'Tema'; - case 'settings.appearance': return 'Utseende'; - case 'settings.videoPlayback': return 'Videouppspelning'; - case 'settings.shufflePlay': return 'Blanda uppspelning'; - case 'settings.advanced': return 'Avancerat'; - case 'settings.useSeasonPostersDescription': return 'Visa säsongsaffisch istället för serieaffisch för avsnitt'; - case 'settings.showHeroSectionDescription': return 'Visa utvalda innehållskarusell på startsidan'; - case 'settings.secondsLabel': return 'Sekunder'; - case 'settings.minutesLabel': return 'Minuter'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => 'Ange tid (${min}-${max})'; - case 'settings.systemTheme': return 'System'; - case 'settings.systemThemeDescription': return 'Följ systeminställningar'; - case 'settings.lightTheme': return 'Ljust'; - case 'settings.darkTheme': return 'Mörkt'; - case 'settings.libraryDensity': return 'Biblioteksdensitet'; - case 'settings.compact': return 'Kompakt'; - case 'settings.compactDescription': return 'Mindre kort, fler objekt synliga'; - case 'settings.normal': return 'Normal'; - case 'settings.normalDescription': return 'Standardstorlek'; - case 'settings.comfortable': return 'Bekväm'; - case 'settings.comfortableDescription': return 'Större kort, färre objekt synliga'; - case 'settings.viewMode': return 'Visningsläge'; - case 'settings.gridView': return 'Rutnät'; - case 'settings.gridViewDescription': return 'Visa objekt i rutnätslayout'; - case 'settings.listView': return 'Lista'; - case 'settings.listViewDescription': return 'Visa objekt i listlayout'; - case 'settings.useSeasonPosters': return 'Använd säsongsaffischer'; - case 'settings.showHeroSection': return 'Visa hjältesektion'; - case 'settings.hardwareDecoding': return 'Hårdvaruavkodning'; - case 'settings.hardwareDecodingDescription': return 'Använd hårdvaruacceleration när tillgängligt'; - case 'settings.bufferSize': return 'Bufferstorlek'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return 'Undertext-styling'; - case 'settings.subtitleStylingDescription': return 'Anpassa undertextutseende'; - case 'settings.smallSkipDuration': return 'Kort hoppvaraktighet'; - case 'settings.largeSkipDuration': return 'Lång hoppvaraktighet'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} sekunder'; - case 'settings.defaultSleepTimer': return 'Standard sovtimer'; - case 'settings.minutesUnit': return ({required Object minutes}) => '${minutes} minuter'; - case 'settings.rememberTrackSelections': return 'Kom ihåg spårval per serie/film'; - case 'settings.rememberTrackSelectionsDescription': return 'Spara automatiskt ljud- och undertextspråkpreferenser när du ändrar spår under uppspelning'; - case 'settings.unwatchedOnly': return 'Endast osedda'; - case 'settings.unwatchedOnlyDescription': return 'Inkludera endast osedda avsnitt i blandningskön'; - case 'settings.shuffleOrderNavigation': return 'Blandningsordning-navigation'; - case 'settings.shuffleOrderNavigationDescription': return 'Nästa/föregående knappar följer blandad ordning'; - case 'settings.loopShuffleQueue': return 'Loopa blandningskö'; - case 'settings.loopShuffleQueueDescription': return 'Starta om kö när slutet nås'; - case 'settings.videoPlayerControls': return 'Videospelar-kontroller'; - case 'settings.keyboardShortcuts': return 'Tangentbordsgenvägar'; - case 'settings.keyboardShortcutsDescription': return 'Anpassa tangentbordsgenvägar'; - case 'settings.debugLogging': return 'Felsökningsloggning'; - case 'settings.debugLoggingDescription': return 'Aktivera detaljerad loggning för felsökning'; - case 'settings.viewLogs': return 'Visa loggar'; - case 'settings.viewLogsDescription': return 'Visa applikationsloggar'; - case 'settings.clearCache': return 'Rensa cache'; - case 'settings.clearCacheDescription': return 'Detta rensar alla cachade bilder och data. Appen kan ta längre tid att ladda innehåll efter cache-rensning.'; - case 'settings.clearCacheSuccess': return 'Cache rensad framgångsrikt'; - case 'settings.resetSettings': return 'Återställ inställningar'; - case 'settings.resetSettingsDescription': return 'Detta återställer alla inställningar till standardvärden. Denna åtgärd kan inte ångras.'; - case 'settings.resetSettingsSuccess': return 'Inställningar återställda framgångsrikt'; - case 'settings.shortcutsReset': return 'Genvägar återställda till standard'; - case 'settings.about': return 'Om'; - case 'settings.aboutDescription': return 'Appinformation och licenser'; - case 'settings.updates': return 'Uppdateringar'; - case 'settings.updateAvailable': return 'Uppdatering tillgänglig'; - case 'settings.checkForUpdates': return 'Kontrollera uppdateringar'; - case 'settings.validationErrorEnterNumber': return 'Vänligen ange ett giltigt nummer'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => 'Tiden måste vara mellan ${min} och ${max} ${unit}'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => 'Genväg redan tilldelad ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => 'Genväg uppdaterad för ${action}'; - case 'settings.autoSkip': return 'Auto Hoppa Över'; - case 'settings.autoSkipIntro': return 'Hoppa Över Intro Automatiskt'; - case 'settings.autoSkipIntroDescription': return 'Hoppa automatiskt över intro-markörer efter några sekunder'; - case 'settings.autoSkipCredits': return 'Hoppa Över Credits Automatiskt'; - case 'settings.autoSkipCreditsDescription': return 'Hoppa automatiskt över credits och spela nästa avsnitt'; - case 'settings.autoSkipDelay': return 'Fördröjning Auto Hoppa Över'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => 'Vänta ${seconds} sekunder innan automatisk överhoppning'; - case 'search.hint': return 'Sök filmer, serier, musik...'; - case 'search.tryDifferentTerm': return 'Prova en annan sökterm'; - case 'search.searchYourMedia': return 'Sök i dina media'; - case 'search.enterTitleActorOrKeyword': return 'Ange en titel, skådespelare eller nyckelord'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => 'Sätt genväg för ${actionName}'; - case 'hotkeys.clearShortcut': return 'Rensa genväg'; - case 'pinEntry.enterPin': return 'Ange PIN'; - case 'pinEntry.showPin': return 'Visa PIN'; - case 'pinEntry.hidePin': return 'Dölj PIN'; - case 'fileInfo.title': return 'Filinformation'; - case 'fileInfo.video': return 'Video'; - case 'fileInfo.audio': return 'Ljud'; - case 'fileInfo.file': return 'Fil'; - case 'fileInfo.advanced': return 'Avancerat'; - case 'fileInfo.codec': return 'Kodek'; - case 'fileInfo.resolution': return 'Upplösning'; - case 'fileInfo.bitrate': return 'Bithastighet'; - case 'fileInfo.frameRate': return 'Bildfrekvens'; - case 'fileInfo.aspectRatio': return 'Bildförhållande'; - case 'fileInfo.profile': return 'Profil'; - case 'fileInfo.bitDepth': return 'Bitdjup'; - case 'fileInfo.colorSpace': return 'Färgrymd'; - case 'fileInfo.colorRange': return 'Färgområde'; - case 'fileInfo.colorPrimaries': return 'Färggrunder'; - case 'fileInfo.chromaSubsampling': return 'Kroma-undersampling'; - case 'fileInfo.channels': return 'Kanaler'; - case 'fileInfo.path': return 'Sökväg'; - case 'fileInfo.size': return 'Storlek'; - case 'fileInfo.container': return 'Container'; - case 'fileInfo.duration': return 'Varaktighet'; - case 'fileInfo.optimizedForStreaming': return 'Optimerad för streaming'; - case 'fileInfo.has64bitOffsets': return '64-bit offset'; - case 'mediaMenu.markAsWatched': return 'Markera som sedd'; - case 'mediaMenu.markAsUnwatched': return 'Markera som osedd'; - case 'mediaMenu.removeFromContinueWatching': return 'Ta bort från Fortsätt titta'; - case 'mediaMenu.goToSeries': return 'Gå till serie'; - case 'mediaMenu.goToSeason': return 'Gå till säsong'; - case 'mediaMenu.shufflePlay': return 'Blanda uppspelning'; - case 'mediaMenu.fileInfo': return 'Filinformation'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, film'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV-serie'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return 'sedd'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} procent sedd'; - case 'accessibility.mediaCardUnwatched': return 'osedd'; - case 'accessibility.tapToPlay': return 'Tryck för att spela'; - case 'tooltips.shufflePlay': return 'Blanda uppspelning'; - case 'tooltips.markAsWatched': return 'Markera som sedd'; - case 'tooltips.markAsUnwatched': return 'Markera som osedd'; - case 'videoControls.audioLabel': return 'Ljud'; - case 'videoControls.subtitlesLabel': return 'Undertexter'; - case 'videoControls.resetToZero': return 'Återställ till 0ms'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} spelas senare'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} spelas tidigare'; - case 'videoControls.noOffset': return 'Ingen offset'; - case 'videoControls.letterbox': return 'Letterbox'; - case 'videoControls.fillScreen': return 'Fyll skärm'; - case 'videoControls.stretch': return 'Sträck'; - case 'videoControls.lockRotation': return 'Lås rotation'; - case 'videoControls.unlockRotation': return 'Lås upp rotation'; - case 'videoControls.sleepTimer': return 'Sovtimer'; - case 'videoControls.timerActive': return 'Timer aktiv'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => 'Uppspelningen pausas om ${duration}'; - case 'videoControls.sleepTimerCompleted': return 'Sovtimer slutförd - uppspelning pausad'; - case 'videoControls.playButton': return 'Spela'; - case 'videoControls.pauseButton': return 'Pausa'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Spola bakåt ${seconds} sekunder'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Spola framåt ${seconds} sekunder'; - case 'videoControls.previousButton': return 'Föregående avsnitt'; - case 'videoControls.nextButton': return 'Nästa avsnitt'; - case 'videoControls.previousChapterButton': return 'Föregående kapitel'; - case 'videoControls.nextChapterButton': return 'Nästa kapitel'; - case 'videoControls.volumeButton': return 'Volym'; - case 'videoControls.muteButton': return 'Tysta'; - case 'videoControls.unmuteButton': return 'Slå på ljud'; - case 'videoControls.settingsButton': return 'Videoinställningar'; - case 'videoControls.audioTrackButton': return 'Ljudspår'; - case 'videoControls.subtitlesButton': return 'Undertexter'; - case 'videoControls.chaptersButton': return 'Kapitel'; - case 'videoControls.versionsButton': return 'Videoversioner'; - case 'videoControls.aspectRatioButton': return 'Bildförhållande'; - case 'videoControls.fullscreenButton': return 'Aktivera helskärm'; - case 'videoControls.exitFullscreenButton': return 'Avsluta helskärm'; - case 'videoControls.rotationLockButton': return 'Rotationslås'; - case 'videoControls.timelineSlider': return 'Videotidslinje'; - case 'videoControls.volumeSlider': return 'Volymnivå'; - case 'videoControls.backButton': return 'Tillbaka'; - case 'userStatus.admin': return 'Admin'; - case 'userStatus.restricted': return 'Begränsad'; - case 'userStatus.protected': return 'Skyddad'; - case 'userStatus.current': return 'NUVARANDE'; - case 'messages.markedAsWatched': return 'Markerad som sedd'; - case 'messages.markedAsUnwatched': return 'Markerad som osedd'; - case 'messages.removedFromContinueWatching': return 'Borttagen från Fortsätt titta'; - case 'messages.errorLoading': return ({required Object error}) => 'Fel: ${error}'; - case 'messages.fileInfoNotAvailable': return 'Filinformation inte tillgänglig'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Fel vid laddning av filinformation: ${error}'; - case 'messages.errorLoadingSeries': return 'Fel vid laddning av serie'; - case 'messages.errorLoadingSeason': return 'Fel vid laddning av säsong'; - case 'messages.musicNotSupported': return 'Musikuppspelning stöds inte ännu'; - case 'messages.logsCleared': return 'Loggar rensade'; - case 'messages.logsCopied': return 'Loggar kopierade till urklipp'; - case 'messages.noLogsAvailable': return 'Inga loggar tillgängliga'; - case 'messages.libraryScanning': return ({required Object title}) => 'Skannar "${title}"...'; - case 'messages.libraryScanStarted': return ({required Object title}) => 'Biblioteksskanning startad för "${title}"'; - case 'messages.libraryScanFailed': return ({required Object error}) => 'Misslyckades att skanna bibliotek: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => 'Uppdaterar metadata för "${title}"...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => 'Metadata-uppdatering startad för "${title}"'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}'; - case 'messages.noPlexToken': return 'Ingen Plex-token hittad. Vänligen logga in igen.'; - case 'messages.logoutConfirm': return 'Är du säker på att du vill logga ut?'; - case 'messages.noSeasonsFound': return 'Inga säsonger hittades'; - case 'messages.noEpisodesFound': return 'Inga avsnitt hittades i första säsongen'; - case 'messages.noEpisodesFoundGeneral': return 'Inga avsnitt hittades'; - case 'messages.noResultsFound': return 'Inga resultat hittades'; - case 'messages.sleepTimerSet': return ({required Object label}) => 'Sovtimer inställd för ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => 'Misslyckades att byta till ${displayName}'; - case 'messages.noItemsAvailable': return 'Inga objekt tillgängliga'; - case 'messages.failedToCreatePlayQueue': return 'Det gick inte att skapa uppspelningskö'; - case 'messages.failedToCreatePlayQueueNoItems': return 'Det gick inte att skapa uppspelningskö – inga objekt'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => 'Kunde inte ${action}: ${error}'; - case 'profile.noUsersAvailable': return 'Inga användare tillgängliga'; - case 'subtitlingStyling.stylingOptions': return 'Stilalternativ'; - case 'subtitlingStyling.fontSize': return 'Teckenstorlek'; - case 'subtitlingStyling.textColor': return 'Textfärg'; - case 'subtitlingStyling.borderSize': return 'Kantstorlek'; - case 'subtitlingStyling.borderColor': return 'Kantfärg'; - case 'subtitlingStyling.backgroundOpacity': return 'Bakgrundsopacitet'; - case 'subtitlingStyling.backgroundColor': return 'Bakgrundsfärg'; - case 'dialog.confirmAction': return 'Bekräfta åtgärd'; - case 'dialog.areYouSure': return 'Är du säker på att du vill utföra denna åtgärd?'; - case 'dialog.cancel': return 'Avbryt'; - case 'dialog.playNow': return 'Spela nu'; - case 'discover.title': return 'Upptäck'; - case 'discover.switchProfile': return 'Byt profil'; - case 'discover.switchServer': return 'Byt server'; - case 'discover.logout': return 'Logga ut'; - case 'discover.noContentAvailable': return 'Inget innehåll tillgängligt'; - case 'discover.addMediaToLibraries': return 'Lägg till media till dina bibliotek'; - case 'discover.continueWatching': return 'Fortsätt titta'; - case 'discover.recentlyAdded': return 'Nyligen tillagda'; - case 'discover.play': return 'Spela'; - case 'discover.resume': return 'Återuppta'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => 'Spela S${season}, E${episode}'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => 'Återuppta S${season}, E${episode}'; - case 'discover.pause': return 'Pausa'; - case 'discover.overview': return 'Översikt'; - case 'discover.cast': return 'Rollbesättning'; - case 'discover.seasons': return 'Säsonger'; - case 'discover.studio': return 'Studio'; - case 'discover.rating': return 'Åldersgräns'; - case 'discover.watched': return 'Tittad'; - case 'discover.episodeCount': return ({required Object count}) => '${count} avsnitt'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '${watched}/${total} sedda'; - case 'discover.movie': return 'Film'; - case 'discover.tvShow': return 'TV-serie'; - case 'discover.minutesLeft': return ({required Object minutes}) => '${minutes} min kvar'; - case 'errors.searchFailed': return ({required Object error}) => 'Sökning misslyckades: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => 'Anslutnings-timeout vid laddning ${context}'; - case 'errors.connectionFailed': return 'Kan inte ansluta till Plex-server'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => 'Misslyckades att ladda ${context}: ${error}'; - case 'errors.noClientAvailable': return 'Ingen klient tillgänglig'; - case 'errors.authenticationFailed': return ({required Object error}) => 'Autentisering misslyckades: ${error}'; - case 'errors.couldNotLaunchUrl': return 'Kunde inte öppna autentiserings-URL'; - case 'errors.pleaseEnterToken': return 'Vänligen ange en token'; - case 'errors.invalidToken': return 'Ogiltig token'; - case 'errors.failedToVerifyToken': return ({required Object error}) => 'Misslyckades att verifiera token: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => 'Misslyckades att byta till ${displayName}'; - case 'errors.connectionFailedGeneric': return 'Anslutning misslyckades'; - case 'libraries.title': return 'Bibliotek'; - case 'libraries.scanLibraryFiles': return 'Skanna biblioteksfiler'; - case 'libraries.scanLibrary': return 'Skanna bibliotek'; - case 'libraries.analyze': return 'Analysera'; - case 'libraries.analyzeLibrary': return 'Analysera bibliotek'; - case 'libraries.refreshMetadata': return 'Uppdatera metadata'; - case 'libraries.emptyTrash': return 'Töm papperskorg'; - case 'libraries.emptyingTrash': return ({required Object title}) => 'Tömmer papperskorg för "${title}"...'; - case 'libraries.trashEmptied': return ({required Object title}) => 'Papperskorg tömd för "${title}"'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => 'Misslyckades att tömma papperskorg: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => 'Analyserar "${title}"...'; - case 'libraries.analysisStarted': return ({required Object title}) => 'Analys startad för "${title}"'; - case 'libraries.failedToAnalyze': return ({required Object error}) => 'Misslyckades att analysera bibliotek: ${error}'; - case 'libraries.noLibrariesFound': return 'Inga bibliotek hittades'; - case 'libraries.thisLibraryIsEmpty': return 'Detta bibliotek är tomt'; - case 'libraries.all': return 'Alla'; - case 'libraries.clearAll': return 'Rensa alla'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => 'Är du säker på att du vill skanna "${title}"?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => 'Är du säker på att du vill analysera "${title}"?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => 'Är du säker på att du vill uppdatera metadata för "${title}"?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => 'Är du säker på att du vill tömma papperskorgen för "${title}"?'; - case 'libraries.manageLibraries': return 'Hantera bibliotek'; - case 'libraries.sort': return 'Sortera'; - case 'libraries.sortBy': return 'Sortera efter'; - case 'libraries.filters': return 'Filter'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => 'Laddar bibliotek... (${count} objekt laddade)'; - case 'libraries.confirmActionMessage': return 'Är du säker på att du vill utföra denna åtgärd?'; - case 'libraries.showLibrary': return 'Visa bibliotek'; - case 'libraries.hideLibrary': return 'Dölj bibliotek'; - case 'libraries.libraryOptions': return 'Biblioteksalternativ'; - case 'libraries.content': return 'bibliotekets innehåll'; - case 'libraries.selectLibrary': return 'Välj bibliotek'; - case 'libraries.filtersWithCount': return ({required Object count}) => 'Filter (${count})'; - case 'libraries.noRecommendations': return 'Inga rekommendationer tillgängliga'; - case 'libraries.noCollections': return 'Inga samlingar i det här biblioteket'; - case 'libraries.noFoldersFound': return 'Inga mappar hittades'; - case 'libraries.folders': return 'mappar'; - case 'libraries.tabs.recommended': return 'Rekommenderat'; - case 'libraries.tabs.browse': return 'Bläddra'; - case 'libraries.tabs.collections': return 'Samlingar'; - case 'libraries.tabs.playlists': return 'Spellistor'; - case 'libraries.groupings.all': return 'Alla'; - case 'libraries.groupings.movies': return 'Filmer'; - case 'libraries.groupings.shows': return 'Serier'; - case 'libraries.groupings.seasons': return 'Säsonger'; - case 'libraries.groupings.episodes': return 'Avsnitt'; - case 'libraries.groupings.folders': return 'Mappar'; - case 'about.title': return 'Om'; - case 'about.openSourceLicenses': return 'Öppen källkod-licenser'; - case 'about.versionLabel': return ({required Object version}) => 'Version ${version}'; - case 'about.appDescription': return 'En vacker Plex-klient för Flutter'; - case 'about.viewLicensesDescription': return 'Visa licenser för tredjepartsbibliotek'; - case 'serverSelection.connectingToServer': return 'Ansluter till server...'; - case 'serverSelection.connectingToAllServers': return 'Ansluter till alla servrar...'; - case 'serverSelection.connectToAllServers': return 'Anslut till alla servrar'; - case 'serverSelection.allServerConnectionsFailed': return 'Misslyckades att ansluta till servrar. Kontrollera ditt nätverk och försök igen.'; - case 'serverSelection.foundServers': return ({required Object count}) => 'Hittade ${count} server(ar)'; - case 'serverSelection.orSelectIndividually': return 'Eller välj en server individuellt:'; - case 'serverSelection.serverDebugCopied': return 'Server-felsökningsdata kopierad till urklipp'; - case 'serverSelection.copyDebugData': return 'Kopiera felsökningsdata'; - case 'serverSelection.noServersFound': return 'Inga servrar hittades'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => 'Inga servrar hittades för ${username} (${email})'; - case 'serverSelection.malformedServerData': return ({required Object count}) => 'Hittade ${count} server(ar) med felformaterad data. Inga giltiga servrar tillgängliga.'; - case 'serverSelection.incompleteServerInfo': return 'Vissa servrar har ofullständig information och hoppades över. Vänligen kontrollera ditt Plex.tv-konto.'; - case 'serverSelection.incompleteConnectionInfo': return 'Server-anslutningsinformation är ofullständig. Försök igen.'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => 'Serverinformation är felformaterad: ${message}'; - case 'serverSelection.networkConnectionFailed': return 'Nätverksanslutning misslyckades. Kontrollera din internetanslutning och försök igen.'; - case 'serverSelection.authenticationFailed': return 'Autentisering misslyckades. Logga in igen.'; - case 'serverSelection.plexServiceUnavailable': return 'Plex-tjänst otillgänglig. Försök igen senare.'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => 'Misslyckades att ladda servrar: ${error}'; - case 'hubDetail.title': return 'Titel'; - case 'hubDetail.releaseYear': return 'Utgivningsår'; - case 'hubDetail.dateAdded': return 'Datum tillagd'; - case 'hubDetail.rating': return 'Betyg'; - case 'hubDetail.noItemsFound': return 'Inga objekt hittades'; - case 'logs.title': return 'Loggar'; - case 'logs.clearLogs': return 'Rensa loggar'; - case 'logs.copyLogs': return 'Kopiera loggar'; - case 'logs.exportLogs': return 'Exportera loggar'; - case 'logs.noLogsToShow': return 'Inga loggar att visa'; - case 'logs.error': return 'Fel:'; - case 'logs.stackTrace': return 'Stack trace:'; - case 'licenses.relatedPackages': return 'Relaterade paket'; - case 'licenses.license': return 'Licens'; - case 'licenses.licenseNumber': return ({required Object number}) => 'Licens ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} licenser'; - case 'navigation.home': return 'Hem'; - case 'navigation.search': return 'Sök'; - case 'navigation.libraries': return 'Bibliotek'; - case 'navigation.settings': return 'Inställningar'; - case 'playlists.title': return 'Spellistor'; - case 'playlists.noPlaylists': return 'Inga spellistor hittades'; - case 'playlists.create': return 'Skapa spellista'; - case 'playlists.newPlaylist': return 'Ny spellista'; - case 'playlists.playlistName': return 'Spellistans namn'; - case 'playlists.enterPlaylistName': return 'Ange spellistans namn'; - case 'playlists.edit': return 'Redigera spellista'; - case 'playlists.delete': return 'Ta bort spellista'; - case 'playlists.addTo': return 'Lägg till i spellista'; - case 'playlists.addItems': return 'Lägg till objekt'; - case 'playlists.removeItem': return 'Ta bort från spellista'; - case 'playlists.clearPlaylist': return 'Rensa spellista'; - case 'playlists.playAll': return 'Spela alla'; - case 'playlists.shuffle': return 'Blanda'; - case 'playlists.smartPlaylist': return 'Smart spellista'; - case 'playlists.regularPlaylist': return 'Vanlig spellista'; - case 'playlists.itemCount': return ({required Object count}) => '${count} objekt'; - case 'playlists.oneItem': return '1 objekt'; - case 'playlists.emptyPlaylist': return 'Denna spellista är tom'; - case 'playlists.deleteConfirm': return 'Ta bort spellista?'; - case 'playlists.deleteMessage': return ({required Object name}) => 'Är du säker på att du vill ta bort "${name}"?'; - case 'playlists.created': return 'Spellista skapad'; - case 'playlists.updated': return 'Spellista uppdaterad'; - case 'playlists.deleted': return 'Spellista borttagen'; - case 'playlists.itemAdded': return 'Tillagd i spellista'; - case 'playlists.itemRemoved': return 'Borttagen från spellista'; - case 'playlists.selectPlaylist': return 'Välj spellista'; - case 'playlists.createNewPlaylist': return 'Skapa ny spellista'; - case 'playlists.errorCreating': return 'Det gick inte att skapa spellista'; - case 'playlists.errorDeleting': return 'Det gick inte att ta bort spellista'; - case 'playlists.errorLoading': return 'Det gick inte att ladda spellistor'; - case 'playlists.errorAdding': return 'Det gick inte att lägga till i spellista'; - case 'playlists.errorReordering': return 'Det gick inte att omordna spellisteobjekt'; - case 'playlists.errorRemoving': return 'Det gick inte att ta bort från spellista'; - case 'playlists.playlist': return 'Spellista'; - case 'collections.title': return 'Samlingar'; - case 'collections.collection': return 'Samling'; - case 'collections.empty': return 'Samlingen är tom'; - case 'collections.noItems': return 'Inga objekt i den här samlingen'; - case 'collections.unknownLibrarySection': return 'Kan inte ta bort: okänd bibliotekssektion'; - case 'collections.deleteCollection': return 'Ta bort samling'; - case 'collections.deleteConfirm': return ({required Object title}) => 'Är du säker på att du vill ta bort "${title}"? Detta går inte att ångra.'; - case 'collections.deleted': return 'Samling borttagen'; - case 'collections.deleteFailed': return 'Det gick inte att ta bort samlingen'; - case 'collections.deleteFailedWithError': return ({required Object error}) => 'Det gick inte att ta bort samlingen: ${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => 'Det gick inte att läsa in samlingsobjekt: ${error}'; - case 'collections.addTo': return 'Lägg till i samling'; - case 'collections.selectCollection': return 'Välj samling'; - case 'collections.createNewCollection': return 'Skapa ny samling'; - case 'collections.collectionName': return 'Samlingsnamn'; - case 'collections.enterCollectionName': return 'Ange samlingsnamn'; - case 'collections.addedToCollection': return 'Tillagd i samling'; - case 'collections.errorAddingToCollection': return 'Fel vid tillägg i samling'; - case 'collections.created': return 'Samling skapad'; - case 'collections.removeFromCollection': return 'Ta bort från samling'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => 'Ta bort "${title}" från denna samling?'; - case 'collections.removedFromCollection': return 'Borttagen från samling'; - case 'collections.removeFromCollectionFailed': return 'Misslyckades med att ta bort från samling'; - case 'collections.removeFromCollectionError': return ({required Object error}) => 'Fel vid borttagning från samling: ${error}'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return 'Laddar...'; + case 'auth.signInWithPlex': + return 'Logga in med Plex'; + case 'auth.showQRCode': + return 'Visa QR-kod'; + case 'auth.cancel': + return 'Avbryt'; + case 'auth.authenticate': + return 'Autentisera'; + case 'auth.retry': + return 'Försök igen'; + case 'auth.debugEnterToken': + return 'Debug: Ange Plex-token'; + case 'auth.plexTokenLabel': + return 'Plex-autentiseringstoken'; + case 'auth.plexTokenHint': + return 'Ange din Plex.tv-token'; + case 'auth.authenticationTimeout': + return 'Autentisering tog för lång tid. Försök igen.'; + case 'auth.scanQRCodeInstruction': + return 'Skanna denna QR-kod med en enhet inloggad på Plex för att autentisera.'; + case 'auth.waitingForAuth': + return 'Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.'; + case 'common.cancel': + return 'Avbryt'; + case 'common.save': + return 'Spara'; + case 'common.close': + return 'Stäng'; + case 'common.clear': + return 'Rensa'; + case 'common.reset': + return 'Återställ'; + case 'common.later': + return 'Senare'; + case 'common.submit': + return 'Skicka'; + case 'common.confirm': + return 'Bekräfta'; + case 'common.retry': + return 'Försök igen'; + case 'common.playNow': + return 'Spela nu'; + case 'common.logout': + return 'Logga ut'; + case 'common.online': + return 'Online'; + case 'common.offline': + return 'Offline'; + case 'common.owned': + return 'Egen'; + case 'common.shared': + return 'Delad'; + case 'common.current': + return 'NUVARANDE'; + case 'common.unknown': + return 'Okänd'; + case 'common.refresh': + return 'Uppdatera'; + case 'common.yes': + return 'Ja'; + case 'common.no': + return 'Nej'; + case 'common.server': + return 'Server'; + case 'common.delete': + return 'Ta bort'; + case 'common.shuffle': + return 'Blanda'; + case 'common.addTo': + return 'Lägg till i...'; + case 'screens.licenses': + return 'Licenser'; + case 'screens.selectServer': + return 'Välj server'; + case 'screens.switchProfile': + return 'Byt profil'; + case 'screens.subtitleStyling': + return 'Undertext-styling'; + case 'screens.search': + return 'Sök'; + case 'screens.logs': + return 'Loggar'; + case 'update.available': + return 'Uppdatering tillgänglig'; + case 'update.versionAvailable': + return ({required Object version}) => + 'Version ${version} är tillgänglig'; + case 'update.currentVersion': + return ({required Object version}) => 'Nuvarande: ${version}'; + case 'update.skipVersion': + return 'Hoppa över denna version'; + case 'update.viewRelease': + return 'Visa release'; + case 'update.latestVersion': + return 'Du har den senaste versionen'; + case 'update.checkFailed': + return 'Misslyckades att kontrollera uppdateringar'; + case 'settings.title': + return 'Inställningar'; + case 'settings.language': + return 'Språk'; + case 'settings.theme': + return 'Tema'; + case 'settings.appearance': + return 'Utseende'; + case 'settings.videoPlayback': + return 'Videouppspelning'; + case 'settings.shufflePlay': + return 'Blanda uppspelning'; + case 'settings.advanced': + return 'Avancerat'; + case 'settings.useSeasonPostersDescription': + return 'Visa säsongsaffisch istället för serieaffisch för avsnitt'; + case 'settings.showHeroSectionDescription': + return 'Visa utvalda innehållskarusell på startsidan'; + case 'settings.secondsLabel': + return 'Sekunder'; + case 'settings.minutesLabel': + return 'Minuter'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + 'Ange tid (${min}-${max})'; + case 'settings.systemTheme': + return 'System'; + case 'settings.systemThemeDescription': + return 'Följ systeminställningar'; + case 'settings.lightTheme': + return 'Ljust'; + case 'settings.darkTheme': + return 'Mörkt'; + case 'settings.libraryDensity': + return 'Biblioteksdensitet'; + case 'settings.compact': + return 'Kompakt'; + case 'settings.compactDescription': + return 'Mindre kort, fler objekt synliga'; + case 'settings.normal': + return 'Normal'; + case 'settings.normalDescription': + return 'Standardstorlek'; + case 'settings.comfortable': + return 'Bekväm'; + case 'settings.comfortableDescription': + return 'Större kort, färre objekt synliga'; + case 'settings.viewMode': + return 'Visningsläge'; + case 'settings.gridView': + return 'Rutnät'; + case 'settings.gridViewDescription': + return 'Visa objekt i rutnätslayout'; + case 'settings.listView': + return 'Lista'; + case 'settings.listViewDescription': + return 'Visa objekt i listlayout'; + case 'settings.useSeasonPosters': + return 'Använd säsongsaffischer'; + case 'settings.showHeroSection': + return 'Visa hjältesektion'; + case 'settings.hardwareDecoding': + return 'Hårdvaruavkodning'; + case 'settings.hardwareDecodingDescription': + return 'Använd hårdvaruacceleration när tillgängligt'; + case 'settings.bufferSize': + return 'Bufferstorlek'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return 'Undertext-styling'; + case 'settings.subtitleStylingDescription': + return 'Anpassa undertextutseende'; + case 'settings.smallSkipDuration': + return 'Kort hoppvaraktighet'; + case 'settings.largeSkipDuration': + return 'Lång hoppvaraktighet'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} sekunder'; + case 'settings.defaultSleepTimer': + return 'Standard sovtimer'; + case 'settings.minutesUnit': + return ({required Object minutes}) => '${minutes} minuter'; + case 'settings.rememberTrackSelections': + return 'Kom ihåg spårval per serie/film'; + case 'settings.rememberTrackSelectionsDescription': + return 'Spara automatiskt ljud- och undertextspråkpreferenser när du ändrar spår under uppspelning'; + case 'settings.unwatchedOnly': + return 'Endast osedda'; + case 'settings.unwatchedOnlyDescription': + return 'Inkludera endast osedda avsnitt i blandningskön'; + case 'settings.shuffleOrderNavigation': + return 'Blandningsordning-navigation'; + case 'settings.shuffleOrderNavigationDescription': + return 'Nästa/föregående knappar följer blandad ordning'; + case 'settings.loopShuffleQueue': + return 'Loopa blandningskö'; + case 'settings.loopShuffleQueueDescription': + return 'Starta om kö när slutet nås'; + case 'settings.videoPlayerControls': + return 'Videospelar-kontroller'; + case 'settings.keyboardShortcuts': + return 'Tangentbordsgenvägar'; + case 'settings.keyboardShortcutsDescription': + return 'Anpassa tangentbordsgenvägar'; + case 'settings.debugLogging': + return 'Felsökningsloggning'; + case 'settings.debugLoggingDescription': + return 'Aktivera detaljerad loggning för felsökning'; + case 'settings.viewLogs': + return 'Visa loggar'; + case 'settings.viewLogsDescription': + return 'Visa applikationsloggar'; + case 'settings.clearCache': + return 'Rensa cache'; + case 'settings.clearCacheDescription': + return 'Detta rensar alla cachade bilder och data. Appen kan ta längre tid att ladda innehåll efter cache-rensning.'; + case 'settings.clearCacheSuccess': + return 'Cache rensad framgångsrikt'; + case 'settings.resetSettings': + return 'Återställ inställningar'; + case 'settings.resetSettingsDescription': + return 'Detta återställer alla inställningar till standardvärden. Denna åtgärd kan inte ångras.'; + case 'settings.resetSettingsSuccess': + return 'Inställningar återställda framgångsrikt'; + case 'settings.shortcutsReset': + return 'Genvägar återställda till standard'; + case 'settings.about': + return 'Om'; + case 'settings.aboutDescription': + return 'Appinformation och licenser'; + case 'settings.updates': + return 'Uppdateringar'; + case 'settings.updateAvailable': + return 'Uppdatering tillgänglig'; + case 'settings.checkForUpdates': + return 'Kontrollera uppdateringar'; + case 'settings.validationErrorEnterNumber': + return 'Vänligen ange ett giltigt nummer'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => 'Tiden måste vara mellan ${min} och ${max} ${unit}'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => 'Genväg redan tilldelad ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => 'Genväg uppdaterad för ${action}'; + case 'settings.autoSkip': + return 'Auto Hoppa Över'; + case 'settings.autoSkipIntro': + return 'Hoppa Över Intro Automatiskt'; + case 'settings.autoSkipIntroDescription': + return 'Hoppa automatiskt över intro-markörer efter några sekunder'; + case 'settings.autoSkipCredits': + return 'Hoppa Över Credits Automatiskt'; + case 'settings.autoSkipCreditsDescription': + return 'Hoppa automatiskt över credits och spela nästa avsnitt'; + case 'settings.autoSkipDelay': + return 'Fördröjning Auto Hoppa Över'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => + 'Vänta ${seconds} sekunder innan automatisk överhoppning'; + case 'settings.downloads': + return 'Nedladdningar'; + case 'settings.downloadLocationDescription': + return 'Välj var nedladdat innehåll ska lagras'; + case 'settings.downloadLocationDefault': + return 'Standard (App-lagring)'; + case 'settings.downloadLocationCustom': + return 'Anpassad Plats'; + case 'settings.selectFolder': + return 'Välj Mapp'; + case 'settings.resetToDefault': + return 'Återställ till Standard'; + case 'settings.currentPath': + return ({required Object path}) => 'Nuvarande: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'Anpassad nedladdningsplats stöds inte på iOS'; + case 'settings.downloadLocationChanged': + return 'Nedladdningsplats ändrad'; + case 'settings.downloadLocationReset': + return 'Nedladdningsplats återställd till standard'; + case 'settings.downloadLocationInvalid': + return 'Vald mapp är inte skrivbar'; + case 'settings.downloadLocationSelectError': + return 'Kunde inte välja mapp'; + case 'settings.downloadOnWifiOnly': + return 'Ladda ner endast på WiFi'; + case 'settings.downloadOnWifiOnlyDescription': + return 'Förhindra nedladdningar vid användning av mobildata'; + case 'settings.cellularDownloadBlocked': + return 'Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.'; + case 'search.hint': + return 'Sök filmer, serier, musik...'; + case 'search.tryDifferentTerm': + return 'Prova en annan sökterm'; + case 'search.searchYourMedia': + return 'Sök i dina media'; + case 'search.enterTitleActorOrKeyword': + return 'Ange en titel, skådespelare eller nyckelord'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => + 'Sätt genväg för ${actionName}'; + case 'hotkeys.clearShortcut': + return 'Rensa genväg'; + case 'pinEntry.enterPin': + return 'Ange PIN'; + case 'pinEntry.showPin': + return 'Visa PIN'; + case 'pinEntry.hidePin': + return 'Dölj PIN'; + case 'fileInfo.title': + return 'Filinformation'; + case 'fileInfo.video': + return 'Video'; + case 'fileInfo.audio': + return 'Ljud'; + case 'fileInfo.file': + return 'Fil'; + case 'fileInfo.advanced': + return 'Avancerat'; + case 'fileInfo.codec': + return 'Kodek'; + case 'fileInfo.resolution': + return 'Upplösning'; + case 'fileInfo.bitrate': + return 'Bithastighet'; + case 'fileInfo.frameRate': + return 'Bildfrekvens'; + case 'fileInfo.aspectRatio': + return 'Bildförhållande'; + case 'fileInfo.profile': + return 'Profil'; + case 'fileInfo.bitDepth': + return 'Bitdjup'; + case 'fileInfo.colorSpace': + return 'Färgrymd'; + case 'fileInfo.colorRange': + return 'Färgområde'; + case 'fileInfo.colorPrimaries': + return 'Färggrunder'; + case 'fileInfo.chromaSubsampling': + return 'Kroma-undersampling'; + case 'fileInfo.channels': + return 'Kanaler'; + case 'fileInfo.path': + return 'Sökväg'; + case 'fileInfo.size': + return 'Storlek'; + case 'fileInfo.container': + return 'Container'; + case 'fileInfo.duration': + return 'Varaktighet'; + case 'fileInfo.optimizedForStreaming': + return 'Optimerad för streaming'; + case 'fileInfo.has64bitOffsets': + return '64-bit offset'; + case 'mediaMenu.markAsWatched': + return 'Markera som sedd'; + case 'mediaMenu.markAsUnwatched': + return 'Markera som osedd'; + case 'mediaMenu.removeFromContinueWatching': + return 'Ta bort från Fortsätt titta'; + case 'mediaMenu.goToSeries': + return 'Gå till serie'; + case 'mediaMenu.goToSeason': + return 'Gå till säsong'; + case 'mediaMenu.shufflePlay': + return 'Blanda uppspelning'; + case 'mediaMenu.fileInfo': + return 'Filinformation'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, film'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, TV-serie'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return 'sedd'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '${percent} procent sedd'; + case 'accessibility.mediaCardUnwatched': + return 'osedd'; + case 'accessibility.tapToPlay': + return 'Tryck för att spela'; + case 'tooltips.shufflePlay': + return 'Blanda uppspelning'; + case 'tooltips.markAsWatched': + return 'Markera som sedd'; + case 'tooltips.markAsUnwatched': + return 'Markera som osedd'; + case 'videoControls.audioLabel': + return 'Ljud'; + case 'videoControls.subtitlesLabel': + return 'Undertexter'; + case 'videoControls.resetToZero': + return 'Återställ till 0ms'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} spelas senare'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} spelas tidigare'; + case 'videoControls.noOffset': + return 'Ingen offset'; + case 'videoControls.letterbox': + return 'Letterbox'; + case 'videoControls.fillScreen': + return 'Fyll skärm'; + case 'videoControls.stretch': + return 'Sträck'; + case 'videoControls.lockRotation': + return 'Lås rotation'; + case 'videoControls.unlockRotation': + return 'Lås upp rotation'; + case 'videoControls.sleepTimer': + return 'Sovtimer'; + case 'videoControls.timerActive': + return 'Timer aktiv'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => + 'Uppspelningen pausas om ${duration}'; + case 'videoControls.sleepTimerCompleted': + return 'Sovtimer slutförd - uppspelning pausad'; + case 'videoControls.playButton': + return 'Spela'; + case 'videoControls.pauseButton': + return 'Pausa'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => 'Spola bakåt ${seconds} sekunder'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => + 'Spola framåt ${seconds} sekunder'; + case 'videoControls.previousButton': + return 'Föregående avsnitt'; + case 'videoControls.nextButton': + return 'Nästa avsnitt'; + case 'videoControls.previousChapterButton': + return 'Föregående kapitel'; + case 'videoControls.nextChapterButton': + return 'Nästa kapitel'; + case 'videoControls.volumeButton': + return 'Volym'; + case 'videoControls.muteButton': + return 'Tysta'; + case 'videoControls.unmuteButton': + return 'Slå på ljud'; + case 'videoControls.settingsButton': + return 'Videoinställningar'; + case 'videoControls.audioTrackButton': + return 'Ljudspår'; + case 'videoControls.subtitlesButton': + return 'Undertexter'; + case 'videoControls.chaptersButton': + return 'Kapitel'; + case 'videoControls.versionsButton': + return 'Videoversioner'; + case 'videoControls.aspectRatioButton': + return 'Bildförhållande'; + case 'videoControls.fullscreenButton': + return 'Aktivera helskärm'; + case 'videoControls.exitFullscreenButton': + return 'Avsluta helskärm'; + case 'videoControls.rotationLockButton': + return 'Rotationslås'; + case 'videoControls.timelineSlider': + return 'Videotidslinje'; + case 'videoControls.volumeSlider': + return 'Volymnivå'; + case 'videoControls.backButton': + return 'Tillbaka'; + case 'userStatus.admin': + return 'Admin'; + case 'userStatus.restricted': + return 'Begränsad'; + case 'userStatus.protected': + return 'Skyddad'; + case 'userStatus.current': + return 'NUVARANDE'; + case 'messages.markedAsWatched': + return 'Markerad som sedd'; + case 'messages.markedAsUnwatched': + return 'Markerad som osedd'; + case 'messages.markedAsWatchedOffline': + return 'Markerad som sedd (synkroniseras när online)'; + case 'messages.markedAsUnwatchedOffline': + return 'Markerad som osedd (synkroniseras när online)'; + case 'messages.removedFromContinueWatching': + return 'Borttagen från Fortsätt titta'; + case 'messages.errorLoading': + return ({required Object error}) => 'Fel: ${error}'; + case 'messages.fileInfoNotAvailable': + return 'Filinformation inte tillgänglig'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => + 'Fel vid laddning av filinformation: ${error}'; + case 'messages.errorLoadingSeries': + return 'Fel vid laddning av serie'; + case 'messages.errorLoadingSeason': + return 'Fel vid laddning av säsong'; + case 'messages.musicNotSupported': + return 'Musikuppspelning stöds inte ännu'; + case 'messages.logsCleared': + return 'Loggar rensade'; + case 'messages.logsCopied': + return 'Loggar kopierade till urklipp'; + case 'messages.noLogsAvailable': + return 'Inga loggar tillgängliga'; + case 'messages.libraryScanning': + return ({required Object title}) => 'Skannar "${title}"...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => + 'Biblioteksskanning startad för "${title}"'; + case 'messages.libraryScanFailed': + return ({required Object error}) => + 'Misslyckades att skanna bibliotek: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => + 'Uppdaterar metadata för "${title}"...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => + 'Metadata-uppdatering startad för "${title}"'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => + 'Misslyckades att uppdatera metadata: ${error}'; + case 'messages.noPlexToken': + return 'Ingen Plex-token hittad. Vänligen logga in igen.'; + case 'messages.logoutConfirm': + return 'Är du säker på att du vill logga ut?'; + case 'messages.noSeasonsFound': + return 'Inga säsonger hittades'; + case 'messages.noEpisodesFound': + return 'Inga avsnitt hittades i första säsongen'; + case 'messages.noEpisodesFoundGeneral': + return 'Inga avsnitt hittades'; + case 'messages.noResultsFound': + return 'Inga resultat hittades'; + case 'messages.sleepTimerSet': + return ({required Object label}) => 'Sovtimer inställd för ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => + 'Misslyckades att byta till ${displayName}'; + case 'messages.noItemsAvailable': + return 'Inga objekt tillgängliga'; + case 'messages.failedToCreatePlayQueue': + return 'Det gick inte att skapa uppspelningskö'; + case 'messages.failedToCreatePlayQueueNoItems': + return 'Det gick inte att skapa uppspelningskö – inga objekt'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + 'Kunde inte ${action}: ${error}'; + case 'messages.youAreOffline': + return 'Du är offline'; + case 'messages.offlineFeatureUnavailable': + return 'Denna funktion kräver en internetanslutning. Dina nedladdningar och inställningar är fortfarande tillgängliga.'; + case 'profile.noUsersAvailable': + return 'Inga användare tillgängliga'; + case 'subtitlingStyling.stylingOptions': + return 'Stilalternativ'; + case 'subtitlingStyling.fontSize': + return 'Teckenstorlek'; + case 'subtitlingStyling.textColor': + return 'Textfärg'; + case 'subtitlingStyling.borderSize': + return 'Kantstorlek'; + case 'subtitlingStyling.borderColor': + return 'Kantfärg'; + case 'subtitlingStyling.backgroundOpacity': + return 'Bakgrundsopacitet'; + case 'subtitlingStyling.backgroundColor': + return 'Bakgrundsfärg'; + case 'dialog.confirmAction': + return 'Bekräfta åtgärd'; + case 'dialog.areYouSure': + return 'Är du säker på att du vill utföra denna åtgärd?'; + case 'dialog.cancel': + return 'Avbryt'; + case 'dialog.playNow': + return 'Spela nu'; + case 'discover.title': + return 'Upptäck'; + case 'discover.switchProfile': + return 'Byt profil'; + case 'discover.switchServer': + return 'Byt server'; + case 'discover.logout': + return 'Logga ut'; + case 'discover.noContentAvailable': + return 'Inget innehåll tillgängligt'; + case 'discover.addMediaToLibraries': + return 'Lägg till media till dina bibliotek'; + case 'discover.continueWatching': + return 'Fortsätt titta'; + case 'discover.recentlyAdded': + return 'Nyligen tillagda'; + case 'discover.play': + return 'Spela'; + case 'discover.resume': + return 'Återuppta'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return 'Pausa'; + case 'discover.overview': + return 'Översikt'; + case 'discover.cast': + return 'Rollbesättning'; + case 'discover.seasons': + return 'Säsonger'; + case 'discover.studio': + return 'Studio'; + case 'discover.rating': + return 'Åldersgräns'; + case 'discover.watched': + return 'Tittad'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} avsnitt'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '${watched}/${total} sedda'; + case 'discover.movie': + return 'Film'; + case 'discover.tvShow': + return 'TV-serie'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '${minutes} min kvar'; + case 'errors.searchFailed': + return ({required Object error}) => 'Sökning misslyckades: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => + 'Anslutnings-timeout vid laddning ${context}'; + case 'errors.connectionFailed': + return 'Kan inte ansluta till Plex-server'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + 'Misslyckades att ladda ${context}: ${error}'; + case 'errors.noClientAvailable': + return 'Ingen klient tillgänglig'; + case 'errors.authenticationFailed': + return ({required Object error}) => + 'Autentisering misslyckades: ${error}'; + case 'errors.couldNotLaunchUrl': + return 'Kunde inte öppna autentiserings-URL'; + case 'errors.pleaseEnterToken': + return 'Vänligen ange en token'; + case 'errors.invalidToken': + return 'Ogiltig token'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => + 'Misslyckades att verifiera token: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => + 'Misslyckades att byta till ${displayName}'; + case 'errors.connectionFailedGeneric': + return 'Anslutning misslyckades'; + case 'libraries.title': + return 'Bibliotek'; + case 'libraries.scanLibraryFiles': + return 'Skanna biblioteksfiler'; + case 'libraries.scanLibrary': + return 'Skanna bibliotek'; + case 'libraries.analyze': + return 'Analysera'; + case 'libraries.analyzeLibrary': + return 'Analysera bibliotek'; + case 'libraries.refreshMetadata': + return 'Uppdatera metadata'; + case 'libraries.emptyTrash': + return 'Töm papperskorg'; + case 'libraries.emptyingTrash': + return ({required Object title}) => + 'Tömmer papperskorg för "${title}"...'; + case 'libraries.trashEmptied': + return ({required Object title}) => 'Papperskorg tömd för "${title}"'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => + 'Misslyckades att tömma papperskorg: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => 'Analyserar "${title}"...'; + case 'libraries.analysisStarted': + return ({required Object title}) => 'Analys startad för "${title}"'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => + 'Misslyckades att analysera bibliotek: ${error}'; + case 'libraries.noLibrariesFound': + return 'Inga bibliotek hittades'; + case 'libraries.thisLibraryIsEmpty': + return 'Detta bibliotek är tomt'; + case 'libraries.all': + return 'Alla'; + case 'libraries.clearAll': + return 'Rensa alla'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => + 'Är du säker på att du vill skanna "${title}"?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => + 'Är du säker på att du vill analysera "${title}"?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => + 'Är du säker på att du vill uppdatera metadata för "${title}"?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => + 'Är du säker på att du vill tömma papperskorgen för "${title}"?'; + case 'libraries.manageLibraries': + return 'Hantera bibliotek'; + case 'libraries.sort': + return 'Sortera'; + case 'libraries.sortBy': + return 'Sortera efter'; + case 'libraries.filters': + return 'Filter'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => + 'Laddar bibliotek... (${count} objekt laddade)'; + case 'libraries.confirmActionMessage': + return 'Är du säker på att du vill utföra denna åtgärd?'; + case 'libraries.showLibrary': + return 'Visa bibliotek'; + case 'libraries.hideLibrary': + return 'Dölj bibliotek'; + case 'libraries.libraryOptions': + return 'Biblioteksalternativ'; + case 'libraries.content': + return 'bibliotekets innehåll'; + case 'libraries.selectLibrary': + return 'Välj bibliotek'; + case 'libraries.filtersWithCount': + return ({required Object count}) => 'Filter (${count})'; + case 'libraries.noRecommendations': + return 'Inga rekommendationer tillgängliga'; + case 'libraries.noCollections': + return 'Inga samlingar i det här biblioteket'; + case 'libraries.noFoldersFound': + return 'Inga mappar hittades'; + case 'libraries.folders': + return 'mappar'; + case 'libraries.tabs.recommended': + return 'Rekommenderat'; + case 'libraries.tabs.browse': + return 'Bläddra'; + case 'libraries.tabs.collections': + return 'Samlingar'; + case 'libraries.tabs.playlists': + return 'Spellistor'; + case 'libraries.groupings.all': + return 'Alla'; + case 'libraries.groupings.movies': + return 'Filmer'; + case 'libraries.groupings.shows': + return 'Serier'; + case 'libraries.groupings.seasons': + return 'Säsonger'; + case 'libraries.groupings.episodes': + return 'Avsnitt'; + case 'libraries.groupings.folders': + return 'Mappar'; + case 'about.title': + return 'Om'; + case 'about.openSourceLicenses': + return 'Öppen källkod-licenser'; + case 'about.versionLabel': + return ({required Object version}) => 'Version ${version}'; + case 'about.appDescription': + return 'En vacker Plex-klient för Flutter'; + case 'about.viewLicensesDescription': + return 'Visa licenser för tredjepartsbibliotek'; + case 'serverSelection.connectingToServer': + return 'Ansluter till server...'; + case 'serverSelection.connectingToAllServers': + return 'Ansluter till alla servrar...'; + case 'serverSelection.connectToAllServers': + return 'Anslut till alla servrar'; + case 'serverSelection.allServerConnectionsFailed': + return 'Misslyckades att ansluta till servrar. Kontrollera ditt nätverk och försök igen.'; + case 'serverSelection.foundServers': + return ({required Object count}) => 'Hittade ${count} server(ar)'; + case 'serverSelection.orSelectIndividually': + return 'Eller välj en server individuellt:'; + case 'serverSelection.serverDebugCopied': + return 'Server-felsökningsdata kopierad till urklipp'; + case 'serverSelection.copyDebugData': + return 'Kopiera felsökningsdata'; + case 'serverSelection.noServersFound': + return 'Inga servrar hittades'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + 'Inga servrar hittades för ${username} (${email})'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + 'Hittade ${count} server(ar) med felformaterad data. Inga giltiga servrar tillgängliga.'; + case 'serverSelection.incompleteServerInfo': + return 'Vissa servrar har ofullständig information och hoppades över. Vänligen kontrollera ditt Plex.tv-konto.'; + case 'serverSelection.incompleteConnectionInfo': + return 'Server-anslutningsinformation är ofullständig. Försök igen.'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => + 'Serverinformation är felformaterad: ${message}'; + case 'serverSelection.networkConnectionFailed': + return 'Nätverksanslutning misslyckades. Kontrollera din internetanslutning och försök igen.'; + case 'serverSelection.authenticationFailed': + return 'Autentisering misslyckades. Logga in igen.'; + case 'serverSelection.plexServiceUnavailable': + return 'Plex-tjänst otillgänglig. Försök igen senare.'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => + 'Misslyckades att ladda servrar: ${error}'; + case 'hubDetail.title': + return 'Titel'; + case 'hubDetail.releaseYear': + return 'Utgivningsår'; + case 'hubDetail.dateAdded': + return 'Datum tillagd'; + case 'hubDetail.rating': + return 'Betyg'; + case 'hubDetail.noItemsFound': + return 'Inga objekt hittades'; + case 'logs.title': + return 'Loggar'; + case 'logs.clearLogs': + return 'Rensa loggar'; + case 'logs.copyLogs': + return 'Kopiera loggar'; + case 'logs.exportLogs': + return 'Exportera loggar'; + case 'logs.noLogsToShow': + return 'Inga loggar att visa'; + case 'logs.error': + return 'Fel:'; + case 'logs.stackTrace': + return 'Stack trace:'; + case 'licenses.relatedPackages': + return 'Relaterade paket'; + case 'licenses.license': + return 'Licens'; + case 'licenses.licenseNumber': + return ({required Object number}) => 'Licens ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} licenser'; + case 'navigation.home': + return 'Hem'; + case 'navigation.search': + return 'Sök'; + case 'navigation.libraries': + return 'Bibliotek'; + case 'navigation.settings': + return 'Inställningar'; + case 'navigation.downloads': + return 'Nedladdningar'; + case 'downloads.title': + return 'Nedladdningar'; + case 'downloads.manage': + return 'Hantera'; + case 'downloads.tvShows': + return 'TV-serier'; + case 'downloads.movies': + return 'Filmer'; + case 'downloads.queue': + return 'Kö'; + case 'downloads.noDownloads': + return 'Inga nedladdningar ännu'; + case 'downloads.noDownloadsDescription': + return 'Nedladdat innehåll visas här för offline-visning'; + case 'downloads.browseContent': + return 'Bläddra i innehåll'; + case 'downloads.downloading': + return 'Laddar ner'; + case 'downloads.downloaded': + return 'Nedladdad'; + case 'downloads.paused': + return 'Pausad'; + case 'downloads.failed': + return 'Misslyckades'; + case 'downloads.queued': + return 'I kö'; + case 'downloads.downloadNow': + return 'Ladda ner'; + case 'downloads.deleteDownload': + return 'Ta bort nedladdning'; + case 'downloads.cancelDownload': + return 'Avbryt nedladdning'; + case 'downloads.pauseDownload': + return 'Pausa nedladdning'; + case 'downloads.resumeDownload': + return 'Återuppta nedladdning'; + case 'downloads.retryDownload': + return 'Försök igen'; + case 'downloads.downloadComplete': + return 'Nedladdning klar'; + case 'downloads.downloadFailed': + return 'Nedladdning misslyckades'; + case 'downloads.downloadCancelled': + return 'Nedladdning avbruten'; + case 'downloads.downloadQueued': + return 'Nedladdning köad'; + case 'downloads.episodesQueued': + return ({required Object count}) => + '${count} avsnitt köade för nedladdning'; + case 'downloads.downloadDeleted': + return 'Nedladdning borttagen'; + case 'downloads.deleteConfirmTitle': + return 'Ta bort nedladdning?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + 'Är du säker på att du vill ta bort "${title}"? Den nedladdade filen kommer att tas bort från din enhet.'; + case 'downloads.deleteConfirmMessage': + return 'Den nedladdade filen kommer att tas bort från din enhet.'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => 'Tar bort ${title}... (${current} av ${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => 'Tar bort ${title}...'; + case 'downloads.storageUsed': + return 'Lagring använd'; + case 'downloads.availableOffline': + return 'Tillgänglig offline'; + case 'downloads.downloadingSeason': + return 'Laddar ner säsong'; + case 'downloads.downloadingEpisode': + return 'Laddar ner avsnitt'; + case 'offline.youAreOffline': + return 'Du är offline'; + case 'offline.offlineMode': + return 'Offline-läge'; + case 'offline.backOnline': + return 'Tillbaka online'; + case 'offline.showingDownloadedContent': + return 'Visar endast nedladdat innehåll'; + case 'offline.noInternetConnection': + return 'Ingen internetanslutning'; + case 'offline.serversUnreachable': + return 'Plex-servrar kan inte nås'; + case 'offline.connectToDownload': + return 'Anslut till internet för att ladda ner innehåll'; + case 'playlists.title': + return 'Spellistor'; + case 'playlists.noPlaylists': + return 'Inga spellistor hittades'; + case 'playlists.create': + return 'Skapa spellista'; + case 'playlists.newPlaylist': + return 'Ny spellista'; + case 'playlists.playlistName': + return 'Spellistans namn'; + case 'playlists.enterPlaylistName': + return 'Ange spellistans namn'; + case 'playlists.edit': + return 'Redigera spellista'; + case 'playlists.delete': + return 'Ta bort spellista'; + case 'playlists.addTo': + return 'Lägg till i spellista'; + case 'playlists.addItems': + return 'Lägg till objekt'; + case 'playlists.removeItem': + return 'Ta bort från spellista'; + case 'playlists.clearPlaylist': + return 'Rensa spellista'; + case 'playlists.playAll': + return 'Spela alla'; + case 'playlists.shuffle': + return 'Blanda'; + case 'playlists.smartPlaylist': + return 'Smart spellista'; + case 'playlists.regularPlaylist': + return 'Vanlig spellista'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} objekt'; + case 'playlists.oneItem': + return '1 objekt'; + case 'playlists.emptyPlaylist': + return 'Denna spellista är tom'; + case 'playlists.deleteConfirm': + return 'Ta bort spellista?'; + case 'playlists.deleteMessage': + return ({required Object name}) => + 'Är du säker på att du vill ta bort "${name}"?'; + case 'playlists.created': + return 'Spellista skapad'; + case 'playlists.updated': + return 'Spellista uppdaterad'; + case 'playlists.deleted': + return 'Spellista borttagen'; + case 'playlists.itemAdded': + return 'Tillagd i spellista'; + case 'playlists.itemRemoved': + return 'Borttagen från spellista'; + case 'playlists.selectPlaylist': + return 'Välj spellista'; + case 'playlists.createNewPlaylist': + return 'Skapa ny spellista'; + case 'playlists.errorCreating': + return 'Det gick inte att skapa spellista'; + case 'playlists.errorDeleting': + return 'Det gick inte att ta bort spellista'; + case 'playlists.errorLoading': + return 'Det gick inte att ladda spellistor'; + case 'playlists.errorAdding': + return 'Det gick inte att lägga till i spellista'; + case 'playlists.errorReordering': + return 'Det gick inte att omordna spellisteobjekt'; + case 'playlists.errorRemoving': + return 'Det gick inte att ta bort från spellista'; + case 'playlists.playlist': + return 'Spellista'; + case 'collections.title': + return 'Samlingar'; + case 'collections.collection': + return 'Samling'; + case 'collections.empty': + return 'Samlingen är tom'; + case 'collections.noItems': + return 'Inga objekt i den här samlingen'; + case 'collections.unknownLibrarySection': + return 'Kan inte ta bort: okänd bibliotekssektion'; + case 'collections.deleteCollection': + return 'Ta bort samling'; + case 'collections.deleteConfirm': + return ({required Object title}) => + 'Är du säker på att du vill ta bort "${title}"? Detta går inte att ångra.'; + case 'collections.deleted': + return 'Samling borttagen'; + case 'collections.deleteFailed': + return 'Det gick inte att ta bort samlingen'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => + 'Det gick inte att ta bort samlingen: ${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => + 'Det gick inte att läsa in samlingsobjekt: ${error}'; + case 'collections.addTo': + return 'Lägg till i samling'; + case 'collections.selectCollection': + return 'Välj samling'; + case 'collections.createNewCollection': + return 'Skapa ny samling'; + case 'collections.collectionName': + return 'Samlingsnamn'; + case 'collections.enterCollectionName': + return 'Ange samlingsnamn'; + case 'collections.addedToCollection': + return 'Tillagd i samling'; + case 'collections.errorAddingToCollection': + return 'Fel vid tillägg i samling'; + case 'collections.created': + return 'Samling skapad'; + case 'collections.removeFromCollection': + return 'Ta bort från samling'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => + 'Ta bort "${title}" från denna samling?'; + case 'collections.removedFromCollection': + return 'Borttagen från samling'; + case 'collections.removeFromCollectionFailed': + return 'Misslyckades med att ta bort från samling'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => + 'Fel vid borttagning från samling: ${error}'; + default: + return null; + } + } } extension on _StringsZh { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'app.title': return 'Plezy'; - case 'app.loading': return '加载中...'; - case 'auth.signInWithPlex': return '使用 Plex 登录'; - case 'auth.showQRCode': return '显示二维码'; - case 'auth.cancel': return '取消'; - case 'auth.authenticate': return '验证'; - case 'auth.retry': return '重试'; - case 'auth.debugEnterToken': return '调试:输入 Plex Token'; - case 'auth.plexTokenLabel': return 'Plex 授权令牌 (Auth Token)'; - case 'auth.plexTokenHint': return '输入你的 Plex.tv 令牌'; - case 'auth.authenticationTimeout': return '验证超时。请重试。'; - case 'auth.scanQRCodeInstruction': return '请使用已登录 Plex 的设备扫描此二维码进行验证。'; - case 'auth.waitingForAuth': return '等待验证中...\n请在你的浏览器中完成登录。'; - case 'common.cancel': return '取消'; - case 'common.save': return '保存'; - case 'common.close': return '关闭'; - case 'common.clear': return '清除'; - case 'common.reset': return '重置'; - case 'common.later': return '稍后'; - case 'common.submit': return '提交'; - case 'common.confirm': return '确认'; - case 'common.retry': return '重试'; - case 'common.playNow': return '立即播放'; - case 'common.logout': return '登出'; - case 'common.online': return '在线'; - case 'common.offline': return '离线'; - case 'common.owned': return '拥有'; - case 'common.shared': return '共享'; - case 'common.current': return '当前'; - case 'common.unknown': return '未知'; - case 'common.refresh': return '刷新'; - case 'common.yes': return '是'; - case 'common.no': return '否'; - case 'common.server': return '服务器'; - case 'common.delete': return '删除'; - case 'common.shuffle': return '随机播放'; - case 'common.addTo': return '添加到...'; - case 'screens.licenses': return '许可证'; - case 'screens.selectServer': return '选择服务器'; - case 'screens.switchProfile': return '切换配置文件'; - case 'screens.subtitleStyling': return '字幕样式'; - case 'screens.search': return '搜索'; - case 'screens.logs': return '日志'; - case 'update.available': return '有可用更新'; - case 'update.versionAvailable': return ({required Object version}) => '版本 ${version} 已发布'; - case 'update.currentVersion': return ({required Object version}) => '当前版本: ${version}'; - case 'update.skipVersion': return '跳过此版本'; - case 'update.viewRelease': return '查看发布详情'; - case 'update.latestVersion': return '已安装的版本是可用的最新版本'; - case 'update.checkFailed': return '无法检查更新'; - case 'settings.title': return '设置'; - case 'settings.language': return '语言'; - case 'settings.theme': return '主题'; - case 'settings.appearance': return '外观'; - case 'settings.videoPlayback': return '视频播放'; - case 'settings.shufflePlay': return '随机播放'; - case 'settings.advanced': return '高级'; - case 'settings.useSeasonPostersDescription': return '为剧集显示季海报而非剧集海报'; - case 'settings.showHeroSectionDescription': return '在主屏幕上显示精选内容轮播区'; - case 'settings.secondsLabel': return '秒'; - case 'settings.minutesLabel': return '分钟'; - case 'settings.secondsShort': return 's'; - case 'settings.minutesShort': return 'm'; - case 'settings.durationHint': return ({required Object min, required Object max}) => '输入时长 (${min}-${max})'; - case 'settings.systemTheme': return '系统'; - case 'settings.systemThemeDescription': return '跟随系统设置'; - case 'settings.lightTheme': return '浅色'; - case 'settings.darkTheme': return '深色'; - case 'settings.libraryDensity': return '媒体库密度'; - case 'settings.compact': return '紧凑'; - case 'settings.compactDescription': return '卡片更小,显示更多项目'; - case 'settings.normal': return '标准'; - case 'settings.normalDescription': return '默认尺寸'; - case 'settings.comfortable': return '舒适'; - case 'settings.comfortableDescription': return '卡片更大,显示更少项目'; - case 'settings.viewMode': return '视图模式'; - case 'settings.gridView': return '网格视图'; - case 'settings.gridViewDescription': return '以网格布局显示项目'; - case 'settings.listView': return '列表视图'; - case 'settings.listViewDescription': return '以列表布局显示项目'; - case 'settings.useSeasonPosters': return '使用季海报'; - case 'settings.showHeroSection': return '显示主要精选区'; - case 'settings.hardwareDecoding': return '硬件解码'; - case 'settings.hardwareDecodingDescription': return '如果可用,使用硬件加速'; - case 'settings.bufferSize': return '缓冲区大小'; - case 'settings.bufferSizeMB': return ({required Object size}) => '${size}MB'; - case 'settings.subtitleStyling': return '字幕样式'; - case 'settings.subtitleStylingDescription': return '调整字幕外观'; - case 'settings.smallSkipDuration': return '短跳过时长'; - case 'settings.largeSkipDuration': return '长跳过时长'; - case 'settings.secondsUnit': return ({required Object seconds}) => '${seconds} 秒'; - case 'settings.defaultSleepTimer': return '默认睡眠定时器'; - case 'settings.minutesUnit': return ({required Object minutes}) => '${minutes} 分钟'; - case 'settings.rememberTrackSelections': return '记住每个剧集/电影的音轨选择'; - case 'settings.rememberTrackSelectionsDescription': return '在播放过程中更改音轨时自动保存音频和字幕语言偏好'; - case 'settings.unwatchedOnly': return '仅未观看'; - case 'settings.unwatchedOnlyDescription': return '随机队列中仅包含未观看的剧集'; - case 'settings.shuffleOrderNavigation': return '随机顺序导航'; - case 'settings.shuffleOrderNavigationDescription': return '下一集/上一集按钮遵循随机播放顺序'; - case 'settings.loopShuffleQueue': return '循环随机队列'; - case 'settings.loopShuffleQueueDescription': return '在队列结束时重新开始'; - case 'settings.videoPlayerControls': return '视频播放器控制'; - case 'settings.keyboardShortcuts': return '键盘快捷键'; - case 'settings.keyboardShortcutsDescription': return '自定义键盘快捷键'; - case 'settings.debugLogging': return '调试日志'; - case 'settings.debugLoggingDescription': return '启用详细日志记录以便故障排除'; - case 'settings.viewLogs': return '查看日志'; - case 'settings.viewLogsDescription': return '查看应用程序日志'; - case 'settings.clearCache': return '清除缓存'; - case 'settings.clearCacheDescription': return '这将清除所有缓存的图片和数据。清除缓存后,应用程序加载内容可能会变慢。'; - case 'settings.clearCacheSuccess': return '缓存清除成功'; - case 'settings.resetSettings': return '重置设置'; - case 'settings.resetSettingsDescription': return '这会将所有设置重置为其默认值。此操作无法撤销。'; - case 'settings.resetSettingsSuccess': return '设置重置成功'; - case 'settings.shortcutsReset': return '快捷键已重置为默认值'; - case 'settings.about': return '关于'; - case 'settings.aboutDescription': return '应用程序信息和许可证'; - case 'settings.updates': return '更新'; - case 'settings.updateAvailable': return '有可用更新'; - case 'settings.checkForUpdates': return '检查更新'; - case 'settings.validationErrorEnterNumber': return '请输入一个有效的数字'; - case 'settings.validationErrorDuration': return ({required Object min, required Object max, required Object unit}) => '时长必须介于 ${min} 和 ${max} ${unit} 之间'; - case 'settings.shortcutAlreadyAssigned': return ({required Object action}) => '快捷键已被分配给 ${action}'; - case 'settings.shortcutUpdated': return ({required Object action}) => '快捷键已为 ${action} 更新'; - case 'settings.autoSkip': return '自动跳过'; - case 'settings.autoSkipIntro': return '自动跳过片头'; - case 'settings.autoSkipIntroDescription': return '几秒钟后自动跳过片头标记'; - case 'settings.autoSkipCredits': return '自动跳过片尾'; - case 'settings.autoSkipCreditsDescription': return '自动跳过片尾并播放下一集'; - case 'settings.autoSkipDelay': return '自动跳过延迟'; - case 'settings.autoSkipDelayDescription': return ({required Object seconds}) => '自动跳过前等待 ${seconds} 秒'; - case 'search.hint': return '搜索电影、系列、音乐...'; - case 'search.tryDifferentTerm': return '尝试不同的搜索词'; - case 'search.searchYourMedia': return '搜索媒体'; - case 'search.enterTitleActorOrKeyword': return '输入标题、演员或关键词'; - case 'hotkeys.setShortcutFor': return ({required Object actionName}) => '为 ${actionName} 设置快捷键'; - case 'hotkeys.clearShortcut': return '清除快捷键'; - case 'pinEntry.enterPin': return '输入 PIN'; - case 'pinEntry.showPin': return '显示 PIN'; - case 'pinEntry.hidePin': return '隐藏 PIN'; - case 'fileInfo.title': return '文件信息'; - case 'fileInfo.video': return '视频'; - case 'fileInfo.audio': return '音频'; - case 'fileInfo.file': return '文件'; - case 'fileInfo.advanced': return '高级'; - case 'fileInfo.codec': return '编解码器'; - case 'fileInfo.resolution': return '分辨率'; - case 'fileInfo.bitrate': return '比特率'; - case 'fileInfo.frameRate': return '帧率'; - case 'fileInfo.aspectRatio': return '宽高比'; - case 'fileInfo.profile': return '配置文件'; - case 'fileInfo.bitDepth': return '位深度'; - case 'fileInfo.colorSpace': return '色彩空间'; - case 'fileInfo.colorRange': return '色彩范围'; - case 'fileInfo.colorPrimaries': return '颜色原色'; - case 'fileInfo.chromaSubsampling': return '色度子采样'; - case 'fileInfo.channels': return '声道'; - case 'fileInfo.path': return '路径'; - case 'fileInfo.size': return '大小'; - case 'fileInfo.container': return '容器'; - case 'fileInfo.duration': return '时长'; - case 'fileInfo.optimizedForStreaming': return '已优化用于流媒体'; - case 'fileInfo.has64bitOffsets': return '64位偏移量'; - case 'mediaMenu.markAsWatched': return '标记为已观看'; - case 'mediaMenu.markAsUnwatched': return '标记为未观看'; - case 'mediaMenu.removeFromContinueWatching': return '从继续观看中移除'; - case 'mediaMenu.goToSeries': return '转到系列'; - case 'mediaMenu.goToSeason': return '转到季'; - case 'mediaMenu.shufflePlay': return '随机播放'; - case 'mediaMenu.fileInfo': return '文件信息'; - case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, 电影'; - case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, 电视剧'; - case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; - case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; - case 'accessibility.mediaCardWatched': return '已观看'; - case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '已观看 ${percent} 百分比'; - case 'accessibility.mediaCardUnwatched': return '未观看'; - case 'accessibility.tapToPlay': return '点击播放'; - case 'tooltips.shufflePlay': return '随机播放'; - case 'tooltips.markAsWatched': return '标记为已观看'; - case 'tooltips.markAsUnwatched': return '标记为未观看'; - case 'videoControls.audioLabel': return '音频'; - case 'videoControls.subtitlesLabel': return '字幕'; - case 'videoControls.resetToZero': return '重置为 0ms'; - case 'videoControls.addTime': return ({required Object amount, required Object unit}) => '+${amount}${unit}'; - case 'videoControls.minusTime': return ({required Object amount, required Object unit}) => '-${amount}${unit}'; - case 'videoControls.playsLater': return ({required Object label}) => '${label} 播放较晚'; - case 'videoControls.playsEarlier': return ({required Object label}) => '${label} 播放较早'; - case 'videoControls.noOffset': return '无偏移'; - case 'videoControls.letterbox': return '信箱模式(Letterbox)'; - case 'videoControls.fillScreen': return '填充屏幕'; - case 'videoControls.stretch': return '拉伸'; - case 'videoControls.lockRotation': return '锁定旋转'; - case 'videoControls.unlockRotation': return '解锁旋转'; - case 'videoControls.sleepTimer': return '睡眠定时器'; - case 'videoControls.timerActive': return '定时器已激活'; - case 'videoControls.playbackWillPauseIn': return ({required Object duration}) => '播放将在 ${duration} 后暂停'; - case 'videoControls.sleepTimerCompleted': return '睡眠定时器已完成 - 播放已暂停'; - case 'videoControls.playButton': return '播放'; - case 'videoControls.pauseButton': return '暂停'; - case 'videoControls.seekBackwardButton': return ({required Object seconds}) => '后退 ${seconds} 秒'; - case 'videoControls.seekForwardButton': return ({required Object seconds}) => '前进 ${seconds} 秒'; - case 'videoControls.previousButton': return '上一集'; - case 'videoControls.nextButton': return '下一集'; - case 'videoControls.previousChapterButton': return '上一章节'; - case 'videoControls.nextChapterButton': return '下一章节'; - case 'videoControls.volumeButton': return '音量'; - case 'videoControls.muteButton': return '静音'; - case 'videoControls.unmuteButton': return '取消静音'; - case 'videoControls.settingsButton': return '视频设置'; - case 'videoControls.audioTrackButton': return '音轨'; - case 'videoControls.subtitlesButton': return '字幕'; - case 'videoControls.chaptersButton': return '章节'; - case 'videoControls.versionsButton': return '视频版本'; - case 'videoControls.aspectRatioButton': return '宽高比'; - case 'videoControls.fullscreenButton': return '进入全屏'; - case 'videoControls.exitFullscreenButton': return '退出全屏'; - case 'videoControls.rotationLockButton': return '旋转锁定'; - case 'videoControls.timelineSlider': return '视频时间轴'; - case 'videoControls.volumeSlider': return '音量调节'; - case 'videoControls.backButton': return '返回'; - case 'userStatus.admin': return '管理员'; - case 'userStatus.restricted': return '受限'; - case 'userStatus.protected': return '受保护'; - case 'userStatus.current': return '当前'; - case 'messages.markedAsWatched': return '已标记为已观看'; - case 'messages.markedAsUnwatched': return '已标记为未观看'; - case 'messages.removedFromContinueWatching': return '已从继续观看中移除'; - case 'messages.errorLoading': return ({required Object error}) => '错误: ${error}'; - case 'messages.fileInfoNotAvailable': return '文件信息不可用'; - case 'messages.errorLoadingFileInfo': return ({required Object error}) => '加载文件信息时出错: ${error}'; - case 'messages.errorLoadingSeries': return '加载系列时出错'; - case 'messages.errorLoadingSeason': return '加载季时出错'; - case 'messages.musicNotSupported': return '尚不支持播放音乐'; - case 'messages.logsCleared': return '日志已清除'; - case 'messages.logsCopied': return '日志已复制到剪贴板'; - case 'messages.noLogsAvailable': return '没有可用日志'; - case 'messages.libraryScanning': return ({required Object title}) => '正在扫描 “${title}”...'; - case 'messages.libraryScanStarted': return ({required Object title}) => '已开始扫描 “${title}” 媒体库'; - case 'messages.libraryScanFailed': return ({required Object error}) => '无法扫描媒体库: ${error}'; - case 'messages.metadataRefreshing': return ({required Object title}) => '正在刷新 “${title}” 的元数据...'; - case 'messages.metadataRefreshStarted': return ({required Object title}) => '已开始刷新 “${title}” 的元数据'; - case 'messages.metadataRefreshFailed': return ({required Object error}) => '无法刷新元数据: ${error}'; - case 'messages.noPlexToken': return '未找到 Plex 令牌。请重新登录。'; - case 'messages.logoutConfirm': return '你确定要登出吗?'; - case 'messages.noSeasonsFound': return '未找到季'; - case 'messages.noEpisodesFound': return '在第一季中未找到剧集'; - case 'messages.noEpisodesFoundGeneral': return '未找到剧集'; - case 'messages.noResultsFound': return '未找到结果'; - case 'messages.sleepTimerSet': return ({required Object label}) => '睡眠定时器已设置为 ${label}'; - case 'messages.failedToSwitchProfile': return ({required Object displayName}) => '无法切换到 ${displayName}'; - case 'messages.noItemsAvailable': return '没有可用的项目'; - case 'messages.failedToCreatePlayQueue': return '创建播放队列失败'; - case 'messages.failedToCreatePlayQueueNoItems': return '创建播放队列失败 - 没有项目'; - case 'messages.failedPlayback': return ({required Object action, required Object error}) => '无法${action}: ${error}'; - case 'profile.noUsersAvailable': return '没有可用用户'; - case 'subtitlingStyling.stylingOptions': return '样式选项'; - case 'subtitlingStyling.fontSize': return '字号'; - case 'subtitlingStyling.textColor': return '文本颜色'; - case 'subtitlingStyling.borderSize': return '边框大小'; - case 'subtitlingStyling.borderColor': return '边框颜色'; - case 'subtitlingStyling.backgroundOpacity': return '背景不透明度'; - case 'subtitlingStyling.backgroundColor': return '背景颜色'; - case 'dialog.confirmAction': return '确认操作'; - case 'dialog.areYouSure': return '确定要执行此操作吗?'; - case 'dialog.cancel': return '取消'; - case 'dialog.playNow': return '立即播放'; - case 'discover.title': return '发现'; - case 'discover.switchProfile': return '切换配置文件'; - case 'discover.switchServer': return '切换服务器'; - case 'discover.logout': return '登出'; - case 'discover.noContentAvailable': return '没有可用内容'; - case 'discover.addMediaToLibraries': return '请向你的媒体库添加一些媒体'; - case 'discover.continueWatching': return '继续观看'; - case 'discover.recentlyAdded': return '最近添加'; - case 'discover.play': return '播放'; - case 'discover.resume': return '继续'; - case 'discover.playEpisode': return ({required Object season, required Object episode}) => '播放 S${season}, E${episode}'; - case 'discover.resumeEpisode': return ({required Object season, required Object episode}) => '继续 S${season}, E${episode}'; - case 'discover.pause': return '暂停'; - case 'discover.overview': return '概述'; - case 'discover.cast': return '演员表'; - case 'discover.seasons': return '季数'; - case 'discover.studio': return '制作公司'; - case 'discover.rating': return '年龄分级'; - case 'discover.watched': return '已观看'; - case 'discover.episodeCount': return ({required Object count}) => '${count} 集'; - case 'discover.watchedProgress': return ({required Object watched, required Object total}) => '已观看 ${watched}/${total} 集'; - case 'discover.movie': return '电影'; - case 'discover.tvShow': return '电视剧'; - case 'discover.minutesLeft': return ({required Object minutes}) => '剩余 ${minutes} 分钟'; - case 'errors.searchFailed': return ({required Object error}) => '搜索失败: ${error}'; - case 'errors.connectionTimeout': return ({required Object context}) => '加载 ${context} 时连接超时'; - case 'errors.connectionFailed': return '无法连接到 Plex 服务器'; - case 'errors.failedToLoad': return ({required Object context, required Object error}) => '无法加载 ${context}: ${error}'; - case 'errors.noClientAvailable': return '没有可用客户端'; - case 'errors.authenticationFailed': return ({required Object error}) => '验证失败: ${error}'; - case 'errors.couldNotLaunchUrl': return '无法打开授权 URL'; - case 'errors.pleaseEnterToken': return '请输入一个令牌'; - case 'errors.invalidToken': return '令牌无效'; - case 'errors.failedToVerifyToken': return ({required Object error}) => '无法验证令牌: ${error}'; - case 'errors.failedToSwitchProfile': return ({required Object displayName}) => '无法切换到 ${displayName}'; - case 'errors.connectionFailedGeneric': return '连接失败'; - case 'libraries.title': return '媒体库'; - case 'libraries.scanLibraryFiles': return '扫描媒体库文件'; - case 'libraries.scanLibrary': return '扫描媒体库'; - case 'libraries.analyze': return '分析'; - case 'libraries.analyzeLibrary': return '分析媒体库'; - case 'libraries.refreshMetadata': return '刷新元数据'; - case 'libraries.emptyTrash': return '清空回收站'; - case 'libraries.emptyingTrash': return ({required Object title}) => '正在清空 “${title}” 的回收站...'; - case 'libraries.trashEmptied': return ({required Object title}) => '已清空 “${title}” 的回收站'; - case 'libraries.failedToEmptyTrash': return ({required Object error}) => '无法清空回收站: ${error}'; - case 'libraries.analyzing': return ({required Object title}) => '正在分析 “${title}”...'; - case 'libraries.analysisStarted': return ({required Object title}) => '已开始分析 “${title}”'; - case 'libraries.failedToAnalyze': return ({required Object error}) => '无法分析媒体库: ${error}'; - case 'libraries.noLibrariesFound': return '未找到媒体库'; - case 'libraries.thisLibraryIsEmpty': return '此媒体库为空'; - case 'libraries.all': return '全部'; - case 'libraries.clearAll': return '全部清除'; - case 'libraries.scanLibraryConfirm': return ({required Object title}) => '确定要扫描 “${title}” 吗?'; - case 'libraries.analyzeLibraryConfirm': return ({required Object title}) => '确定要分析 “${title}” 吗?'; - case 'libraries.refreshMetadataConfirm': return ({required Object title}) => '确定要刷新 “${title}” 的元数据吗?'; - case 'libraries.emptyTrashConfirm': return ({required Object title}) => '确定要清空 “${title}” 的回收站吗?'; - case 'libraries.manageLibraries': return '管理媒体库'; - case 'libraries.sort': return '排序'; - case 'libraries.sortBy': return '排序依据'; - case 'libraries.filters': return '筛选器'; - case 'libraries.loadingLibraryWithCount': return ({required Object count}) => '正在加载媒体库... (已加载 ${count} 个项目)'; - case 'libraries.confirmActionMessage': return '确定要执行此操作吗?'; - case 'libraries.showLibrary': return '显示媒体库'; - case 'libraries.hideLibrary': return '隐藏媒体库'; - case 'libraries.libraryOptions': return '媒体库选项'; - case 'libraries.content': return '媒体库内容'; - case 'libraries.selectLibrary': return '选择媒体库'; - case 'libraries.filtersWithCount': return ({required Object count}) => '筛选器(${count})'; - case 'libraries.noRecommendations': return '暂无推荐'; - case 'libraries.noCollections': return '此媒体库中没有合集'; - case 'libraries.noFoldersFound': return '未找到文件夹'; - case 'libraries.folders': return '文件夹'; - case 'libraries.tabs.recommended': return '推荐'; - case 'libraries.tabs.browse': return '浏览'; - case 'libraries.tabs.collections': return '合集'; - case 'libraries.tabs.playlists': return '播放列表'; - case 'libraries.groupings.all': return '全部'; - case 'libraries.groupings.movies': return '电影'; - case 'libraries.groupings.shows': return '剧集'; - case 'libraries.groupings.seasons': return '季'; - case 'libraries.groupings.episodes': return '集'; - case 'libraries.groupings.folders': return '文件夹'; - case 'about.title': return '关于'; - case 'about.openSourceLicenses': return '开源许可证'; - case 'about.versionLabel': return ({required Object version}) => '版本 ${version}'; - case 'about.appDescription': return '一款精美的 Flutter Plex 客户端'; - case 'about.viewLicensesDescription': return '查看第三方库的许可证'; - case 'serverSelection.connectingToServer': return '正在连接服务器...'; - case 'serverSelection.connectingToAllServers': return '正在连接所有服务器...'; - case 'serverSelection.connectToAllServers': return '连接所有服务器'; - case 'serverSelection.allServerConnectionsFailed': return '无法连接到任何服务器。请检查你的网络并重试。'; - case 'serverSelection.foundServers': return ({required Object count}) => '找到 ${count} 个服务器'; - case 'serverSelection.orSelectIndividually': return '或单独选择一个服务器:'; - case 'serverSelection.serverDebugCopied': return '服务器调试数据已复制到剪贴板'; - case 'serverSelection.copyDebugData': return '复制调试数据'; - case 'serverSelection.noServersFound': return '未找到服务器'; - case 'serverSelection.noServersFoundForAccount': return ({required Object username, required Object email}) => '未找到 ${username} (${email}) 的服务器'; - case 'serverSelection.malformedServerData': return ({required Object count}) => '找到 ${count} 个数据格式错误的服务器。没有可用的有效服务器。'; - case 'serverSelection.incompleteServerInfo': return '某些服务器信息不完整,已被跳过。请检查你的 Plex.tv 账户。'; - case 'serverSelection.incompleteConnectionInfo': return '服务器连接信息不完整。请重试。'; - case 'serverSelection.malformedServerInfo': return ({required Object message}) => '服务器信息格式错误: ${message}'; - case 'serverSelection.networkConnectionFailed': return '网络连接失败。请检查你的网络连接并重试。'; - case 'serverSelection.authenticationFailed': return '验证失败。请重新登录。'; - case 'serverSelection.plexServiceUnavailable': return 'Plex 服务不可用。请稍后重试。'; - case 'serverSelection.failedToLoadServers': return ({required Object error}) => '无法加载服务器: ${error}'; - case 'hubDetail.title': return '标题'; - case 'hubDetail.releaseYear': return '发行年份'; - case 'hubDetail.dateAdded': return '添加日期'; - case 'hubDetail.rating': return '评分'; - case 'hubDetail.noItemsFound': return '未找到项目'; - case 'logs.title': return '日志'; - case 'logs.clearLogs': return '清除日志'; - case 'logs.copyLogs': return '复制日志'; - case 'logs.exportLogs': return '导出日志'; - case 'logs.noLogsToShow': return '没有可显示的日志'; - case 'logs.error': return '错误:'; - case 'logs.stackTrace': return '堆栈跟踪 (Stack Trace):'; - case 'licenses.relatedPackages': return '相关软件包'; - case 'licenses.license': return '许可证'; - case 'licenses.licenseNumber': return ({required Object number}) => '许可证 ${number}'; - case 'licenses.licensesCount': return ({required Object count}) => '${count} 个许可证'; - case 'navigation.home': return '主页'; - case 'navigation.search': return '搜索'; - case 'navigation.libraries': return '媒体库'; - case 'navigation.settings': return '设置'; - case 'playlists.title': return '播放列表'; - case 'playlists.noPlaylists': return '未找到播放列表'; - case 'playlists.create': return '创建播放列表'; - case 'playlists.newPlaylist': return '新播放列表'; - case 'playlists.playlistName': return '播放列表名称'; - case 'playlists.enterPlaylistName': return '输入播放列表名称'; - case 'playlists.edit': return '编辑播放列表'; - case 'playlists.delete': return '删除播放列表'; - case 'playlists.addTo': return '添加到播放列表'; - case 'playlists.addItems': return '添加项目'; - case 'playlists.removeItem': return '从播放列表中移除'; - case 'playlists.clearPlaylist': return '清空播放列表'; - case 'playlists.playAll': return '全部播放'; - case 'playlists.shuffle': return '随机播放'; - case 'playlists.smartPlaylist': return '智能播放列表'; - case 'playlists.regularPlaylist': return '普通播放列表'; - case 'playlists.itemCount': return ({required Object count}) => '${count} 个项目'; - case 'playlists.oneItem': return '1 个项目'; - case 'playlists.emptyPlaylist': return '此播放列表为空'; - case 'playlists.deleteConfirm': return '删除播放列表?'; - case 'playlists.deleteMessage': return ({required Object name}) => '确定要删除 "${name}" 吗?'; - case 'playlists.created': return '播放列表已创建'; - case 'playlists.updated': return '播放列表已更新'; - case 'playlists.deleted': return '播放列表已删除'; - case 'playlists.itemAdded': return '已添加到播放列表'; - case 'playlists.itemRemoved': return '已从播放列表中移除'; - case 'playlists.selectPlaylist': return '选择播放列表'; - case 'playlists.createNewPlaylist': return '创建新播放列表'; - case 'playlists.errorCreating': return '创建播放列表失败'; - case 'playlists.errorDeleting': return '删除播放列表失败'; - case 'playlists.errorLoading': return '加载播放列表失败'; - case 'playlists.errorAdding': return '添加到播放列表失败'; - case 'playlists.errorReordering': return '重新排序播放列表项目失败'; - case 'playlists.errorRemoving': return '从播放列表中移除失败'; - case 'playlists.playlist': return '播放列表'; - case 'collections.title': return '合集'; - case 'collections.collection': return '合集'; - case 'collections.empty': return '合集为空'; - case 'collections.noItems': return '此合集没有项目'; - case 'collections.unknownLibrarySection': return '无法删除:未知的媒体库分区'; - case 'collections.deleteCollection': return '删除合集'; - case 'collections.deleteConfirm': return ({required Object title}) => '确定要删除"${title}"吗?此操作无法撤销。'; - case 'collections.deleted': return '已删除合集'; - case 'collections.deleteFailed': return '删除合集失败'; - case 'collections.deleteFailedWithError': return ({required Object error}) => '删除合集失败:${error}'; - case 'collections.failedToLoadItems': return ({required Object error}) => '加载合集项目失败:${error}'; - case 'collections.addTo': return '添加到合集'; - case 'collections.selectCollection': return '选择合集'; - case 'collections.createNewCollection': return '创建新合集'; - case 'collections.collectionName': return '合集名称'; - case 'collections.enterCollectionName': return '输入合集名称'; - case 'collections.addedToCollection': return '已添加到合集'; - case 'collections.errorAddingToCollection': return '添加到合集失败'; - case 'collections.created': return '已创建合集'; - case 'collections.removeFromCollection': return '从合集移除'; - case 'collections.removeFromCollectionConfirm': return ({required Object title}) => '将“${title}”从此合集移除?'; - case 'collections.removedFromCollection': return '已从合集移除'; - case 'collections.removeFromCollectionFailed': return '从合集移除失败'; - case 'collections.removeFromCollectionError': return ({required Object error}) => '从合集移除时出错:${error}'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'app.title': + return 'Plezy'; + case 'app.loading': + return '加载中...'; + case 'auth.signInWithPlex': + return '使用 Plex 登录'; + case 'auth.showQRCode': + return '显示二维码'; + case 'auth.cancel': + return '取消'; + case 'auth.authenticate': + return '验证'; + case 'auth.retry': + return '重试'; + case 'auth.debugEnterToken': + return '调试:输入 Plex Token'; + case 'auth.plexTokenLabel': + return 'Plex 授权令牌 (Auth Token)'; + case 'auth.plexTokenHint': + return '输入你的 Plex.tv 令牌'; + case 'auth.authenticationTimeout': + return '验证超时。请重试。'; + case 'auth.scanQRCodeInstruction': + return '请使用已登录 Plex 的设备扫描此二维码进行验证。'; + case 'auth.waitingForAuth': + return '等待验证中...\n请在你的浏览器中完成登录。'; + case 'common.cancel': + return '取消'; + case 'common.save': + return '保存'; + case 'common.close': + return '关闭'; + case 'common.clear': + return '清除'; + case 'common.reset': + return '重置'; + case 'common.later': + return '稍后'; + case 'common.submit': + return '提交'; + case 'common.confirm': + return '确认'; + case 'common.retry': + return '重试'; + case 'common.playNow': + return '立即播放'; + case 'common.logout': + return '登出'; + case 'common.online': + return '在线'; + case 'common.offline': + return '离线'; + case 'common.owned': + return '拥有'; + case 'common.shared': + return '共享'; + case 'common.current': + return '当前'; + case 'common.unknown': + return '未知'; + case 'common.refresh': + return '刷新'; + case 'common.yes': + return '是'; + case 'common.no': + return '否'; + case 'common.server': + return '服务器'; + case 'common.delete': + return '删除'; + case 'common.shuffle': + return '随机播放'; + case 'common.addTo': + return '添加到...'; + case 'screens.licenses': + return '许可证'; + case 'screens.selectServer': + return '选择服务器'; + case 'screens.switchProfile': + return '切换配置文件'; + case 'screens.subtitleStyling': + return '字幕样式'; + case 'screens.search': + return '搜索'; + case 'screens.logs': + return '日志'; + case 'update.available': + return '有可用更新'; + case 'update.versionAvailable': + return ({required Object version}) => '版本 ${version} 已发布'; + case 'update.currentVersion': + return ({required Object version}) => '当前版本: ${version}'; + case 'update.skipVersion': + return '跳过此版本'; + case 'update.viewRelease': + return '查看发布详情'; + case 'update.latestVersion': + return '已安装的版本是可用的最新版本'; + case 'update.checkFailed': + return '无法检查更新'; + case 'settings.title': + return '设置'; + case 'settings.language': + return '语言'; + case 'settings.theme': + return '主题'; + case 'settings.appearance': + return '外观'; + case 'settings.videoPlayback': + return '视频播放'; + case 'settings.shufflePlay': + return '随机播放'; + case 'settings.advanced': + return '高级'; + case 'settings.useSeasonPostersDescription': + return '为剧集显示季海报而非剧集海报'; + case 'settings.showHeroSectionDescription': + return '在主屏幕上显示精选内容轮播区'; + case 'settings.secondsLabel': + return '秒'; + case 'settings.minutesLabel': + return '分钟'; + case 'settings.secondsShort': + return 's'; + case 'settings.minutesShort': + return 'm'; + case 'settings.durationHint': + return ({required Object min, required Object max}) => + '输入时长 (${min}-${max})'; + case 'settings.systemTheme': + return '系统'; + case 'settings.systemThemeDescription': + return '跟随系统设置'; + case 'settings.lightTheme': + return '浅色'; + case 'settings.darkTheme': + return '深色'; + case 'settings.libraryDensity': + return '媒体库密度'; + case 'settings.compact': + return '紧凑'; + case 'settings.compactDescription': + return '卡片更小,显示更多项目'; + case 'settings.normal': + return '标准'; + case 'settings.normalDescription': + return '默认尺寸'; + case 'settings.comfortable': + return '舒适'; + case 'settings.comfortableDescription': + return '卡片更大,显示更少项目'; + case 'settings.viewMode': + return '视图模式'; + case 'settings.gridView': + return '网格视图'; + case 'settings.gridViewDescription': + return '以网格布局显示项目'; + case 'settings.listView': + return '列表视图'; + case 'settings.listViewDescription': + return '以列表布局显示项目'; + case 'settings.useSeasonPosters': + return '使用季海报'; + case 'settings.showHeroSection': + return '显示主要精选区'; + case 'settings.hardwareDecoding': + return '硬件解码'; + case 'settings.hardwareDecodingDescription': + return '如果可用,使用硬件加速'; + case 'settings.bufferSize': + return '缓冲区大小'; + case 'settings.bufferSizeMB': + return ({required Object size}) => '${size}MB'; + case 'settings.subtitleStyling': + return '字幕样式'; + case 'settings.subtitleStylingDescription': + return '调整字幕外观'; + case 'settings.smallSkipDuration': + return '短跳过时长'; + case 'settings.largeSkipDuration': + return '长跳过时长'; + case 'settings.secondsUnit': + return ({required Object seconds}) => '${seconds} 秒'; + case 'settings.defaultSleepTimer': + return '默认睡眠定时器'; + case 'settings.minutesUnit': + return ({required Object minutes}) => '${minutes} 分钟'; + case 'settings.rememberTrackSelections': + return '记住每个剧集/电影的音轨选择'; + case 'settings.rememberTrackSelectionsDescription': + return '在播放过程中更改音轨时自动保存音频和字幕语言偏好'; + case 'settings.unwatchedOnly': + return '仅未观看'; + case 'settings.unwatchedOnlyDescription': + return '随机队列中仅包含未观看的剧集'; + case 'settings.shuffleOrderNavigation': + return '随机顺序导航'; + case 'settings.shuffleOrderNavigationDescription': + return '下一集/上一集按钮遵循随机播放顺序'; + case 'settings.loopShuffleQueue': + return '循环随机队列'; + case 'settings.loopShuffleQueueDescription': + return '在队列结束时重新开始'; + case 'settings.videoPlayerControls': + return '视频播放器控制'; + case 'settings.keyboardShortcuts': + return '键盘快捷键'; + case 'settings.keyboardShortcutsDescription': + return '自定义键盘快捷键'; + case 'settings.debugLogging': + return '调试日志'; + case 'settings.debugLoggingDescription': + return '启用详细日志记录以便故障排除'; + case 'settings.viewLogs': + return '查看日志'; + case 'settings.viewLogsDescription': + return '查看应用程序日志'; + case 'settings.clearCache': + return '清除缓存'; + case 'settings.clearCacheDescription': + return '这将清除所有缓存的图片和数据。清除缓存后,应用程序加载内容可能会变慢。'; + case 'settings.clearCacheSuccess': + return '缓存清除成功'; + case 'settings.resetSettings': + return '重置设置'; + case 'settings.resetSettingsDescription': + return '这会将所有设置重置为其默认值。此操作无法撤销。'; + case 'settings.resetSettingsSuccess': + return '设置重置成功'; + case 'settings.shortcutsReset': + return '快捷键已重置为默认值'; + case 'settings.about': + return '关于'; + case 'settings.aboutDescription': + return '应用程序信息和许可证'; + case 'settings.updates': + return '更新'; + case 'settings.updateAvailable': + return '有可用更新'; + case 'settings.checkForUpdates': + return '检查更新'; + case 'settings.validationErrorEnterNumber': + return '请输入一个有效的数字'; + case 'settings.validationErrorDuration': + return ({ + required Object min, + required Object max, + required Object unit, + }) => '时长必须介于 ${min} 和 ${max} ${unit} 之间'; + case 'settings.shortcutAlreadyAssigned': + return ({required Object action}) => '快捷键已被分配给 ${action}'; + case 'settings.shortcutUpdated': + return ({required Object action}) => '快捷键已为 ${action} 更新'; + case 'settings.autoSkip': + return '自动跳过'; + case 'settings.autoSkipIntro': + return '自动跳过片头'; + case 'settings.autoSkipIntroDescription': + return '几秒钟后自动跳过片头标记'; + case 'settings.autoSkipCredits': + return '自动跳过片尾'; + case 'settings.autoSkipCreditsDescription': + return '自动跳过片尾并播放下一集'; + case 'settings.autoSkipDelay': + return '自动跳过延迟'; + case 'settings.autoSkipDelayDescription': + return ({required Object seconds}) => '自动跳过前等待 ${seconds} 秒'; + case 'settings.downloads': + return '下载'; + case 'settings.downloadLocationDescription': + return '选择下载内容的存储位置'; + case 'settings.downloadLocationDefault': + return '默认(应用存储)'; + case 'settings.downloadLocationCustom': + return '自定义位置'; + case 'settings.selectFolder': + return '选择文件夹'; + case 'settings.resetToDefault': + return '重置为默认'; + case 'settings.currentPath': + return ({required Object path}) => '当前: ${path}'; + case 'settings.downloadLocationNotSupported': + return 'iOS 不支持自定义下载位置'; + case 'settings.downloadLocationChanged': + return '下载位置已更改'; + case 'settings.downloadLocationReset': + return '下载位置已重置为默认'; + case 'settings.downloadLocationInvalid': + return '所选文件夹不可写入'; + case 'settings.downloadLocationSelectError': + return '选择文件夹失败'; + case 'settings.downloadOnWifiOnly': + return '仅在 WiFi 时下载'; + case 'settings.downloadOnWifiOnlyDescription': + return '使用蜂窝数据时禁止下载'; + case 'settings.cellularDownloadBlocked': + return '蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。'; + case 'search.hint': + return '搜索电影、系列、音乐...'; + case 'search.tryDifferentTerm': + return '尝试不同的搜索词'; + case 'search.searchYourMedia': + return '搜索媒体'; + case 'search.enterTitleActorOrKeyword': + return '输入标题、演员或关键词'; + case 'hotkeys.setShortcutFor': + return ({required Object actionName}) => '为 ${actionName} 设置快捷键'; + case 'hotkeys.clearShortcut': + return '清除快捷键'; + case 'pinEntry.enterPin': + return '输入 PIN'; + case 'pinEntry.showPin': + return '显示 PIN'; + case 'pinEntry.hidePin': + return '隐藏 PIN'; + case 'fileInfo.title': + return '文件信息'; + case 'fileInfo.video': + return '视频'; + case 'fileInfo.audio': + return '音频'; + case 'fileInfo.file': + return '文件'; + case 'fileInfo.advanced': + return '高级'; + case 'fileInfo.codec': + return '编解码器'; + case 'fileInfo.resolution': + return '分辨率'; + case 'fileInfo.bitrate': + return '比特率'; + case 'fileInfo.frameRate': + return '帧率'; + case 'fileInfo.aspectRatio': + return '宽高比'; + case 'fileInfo.profile': + return '配置文件'; + case 'fileInfo.bitDepth': + return '位深度'; + case 'fileInfo.colorSpace': + return '色彩空间'; + case 'fileInfo.colorRange': + return '色彩范围'; + case 'fileInfo.colorPrimaries': + return '颜色原色'; + case 'fileInfo.chromaSubsampling': + return '色度子采样'; + case 'fileInfo.channels': + return '声道'; + case 'fileInfo.path': + return '路径'; + case 'fileInfo.size': + return '大小'; + case 'fileInfo.container': + return '容器'; + case 'fileInfo.duration': + return '时长'; + case 'fileInfo.optimizedForStreaming': + return '已优化用于流媒体'; + case 'fileInfo.has64bitOffsets': + return '64位偏移量'; + case 'mediaMenu.markAsWatched': + return '标记为已观看'; + case 'mediaMenu.markAsUnwatched': + return '标记为未观看'; + case 'mediaMenu.removeFromContinueWatching': + return '从继续观看中移除'; + case 'mediaMenu.goToSeries': + return '转到系列'; + case 'mediaMenu.goToSeason': + return '转到季'; + case 'mediaMenu.shufflePlay': + return '随机播放'; + case 'mediaMenu.fileInfo': + return '文件信息'; + case 'accessibility.mediaCardMovie': + return ({required Object title}) => '${title}, 电影'; + case 'accessibility.mediaCardShow': + return ({required Object title}) => '${title}, 电视剧'; + case 'accessibility.mediaCardEpisode': + return ({required Object title, required Object episodeInfo}) => + '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': + return ({required Object title, required Object seasonInfo}) => + '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': + return '已观看'; + case 'accessibility.mediaCardPartiallyWatched': + return ({required Object percent}) => '已观看 ${percent} 百分比'; + case 'accessibility.mediaCardUnwatched': + return '未观看'; + case 'accessibility.tapToPlay': + return '点击播放'; + case 'tooltips.shufflePlay': + return '随机播放'; + case 'tooltips.markAsWatched': + return '标记为已观看'; + case 'tooltips.markAsUnwatched': + return '标记为未观看'; + case 'videoControls.audioLabel': + return '音频'; + case 'videoControls.subtitlesLabel': + return '字幕'; + case 'videoControls.resetToZero': + return '重置为 0ms'; + case 'videoControls.addTime': + return ({required Object amount, required Object unit}) => + '+${amount}${unit}'; + case 'videoControls.minusTime': + return ({required Object amount, required Object unit}) => + '-${amount}${unit}'; + case 'videoControls.playsLater': + return ({required Object label}) => '${label} 播放较晚'; + case 'videoControls.playsEarlier': + return ({required Object label}) => '${label} 播放较早'; + case 'videoControls.noOffset': + return '无偏移'; + case 'videoControls.letterbox': + return '信箱模式(Letterbox)'; + case 'videoControls.fillScreen': + return '填充屏幕'; + case 'videoControls.stretch': + return '拉伸'; + case 'videoControls.lockRotation': + return '锁定旋转'; + case 'videoControls.unlockRotation': + return '解锁旋转'; + case 'videoControls.sleepTimer': + return '睡眠定时器'; + case 'videoControls.timerActive': + return '定时器已激活'; + case 'videoControls.playbackWillPauseIn': + return ({required Object duration}) => '播放将在 ${duration} 后暂停'; + case 'videoControls.sleepTimerCompleted': + return '睡眠定时器已完成 - 播放已暂停'; + case 'videoControls.playButton': + return '播放'; + case 'videoControls.pauseButton': + return '暂停'; + case 'videoControls.seekBackwardButton': + return ({required Object seconds}) => '后退 ${seconds} 秒'; + case 'videoControls.seekForwardButton': + return ({required Object seconds}) => '前进 ${seconds} 秒'; + case 'videoControls.previousButton': + return '上一集'; + case 'videoControls.nextButton': + return '下一集'; + case 'videoControls.previousChapterButton': + return '上一章节'; + case 'videoControls.nextChapterButton': + return '下一章节'; + case 'videoControls.volumeButton': + return '音量'; + case 'videoControls.muteButton': + return '静音'; + case 'videoControls.unmuteButton': + return '取消静音'; + case 'videoControls.settingsButton': + return '视频设置'; + case 'videoControls.audioTrackButton': + return '音轨'; + case 'videoControls.subtitlesButton': + return '字幕'; + case 'videoControls.chaptersButton': + return '章节'; + case 'videoControls.versionsButton': + return '视频版本'; + case 'videoControls.aspectRatioButton': + return '宽高比'; + case 'videoControls.fullscreenButton': + return '进入全屏'; + case 'videoControls.exitFullscreenButton': + return '退出全屏'; + case 'videoControls.rotationLockButton': + return '旋转锁定'; + case 'videoControls.timelineSlider': + return '视频时间轴'; + case 'videoControls.volumeSlider': + return '音量调节'; + case 'videoControls.backButton': + return '返回'; + case 'userStatus.admin': + return '管理员'; + case 'userStatus.restricted': + return '受限'; + case 'userStatus.protected': + return '受保护'; + case 'userStatus.current': + return '当前'; + case 'messages.markedAsWatched': + return '已标记为已观看'; + case 'messages.markedAsUnwatched': + return '已标记为未观看'; + case 'messages.markedAsWatchedOffline': + return '已标记为已观看 (将在联网时同步)'; + case 'messages.markedAsUnwatchedOffline': + return '已标记为未观看 (将在联网时同步)'; + case 'messages.removedFromContinueWatching': + return '已从继续观看中移除'; + case 'messages.errorLoading': + return ({required Object error}) => '错误: ${error}'; + case 'messages.fileInfoNotAvailable': + return '文件信息不可用'; + case 'messages.errorLoadingFileInfo': + return ({required Object error}) => '加载文件信息时出错: ${error}'; + case 'messages.errorLoadingSeries': + return '加载系列时出错'; + case 'messages.errorLoadingSeason': + return '加载季时出错'; + case 'messages.musicNotSupported': + return '尚不支持播放音乐'; + case 'messages.logsCleared': + return '日志已清除'; + case 'messages.logsCopied': + return '日志已复制到剪贴板'; + case 'messages.noLogsAvailable': + return '没有可用日志'; + case 'messages.libraryScanning': + return ({required Object title}) => '正在扫描 “${title}”...'; + case 'messages.libraryScanStarted': + return ({required Object title}) => '已开始扫描 “${title}” 媒体库'; + case 'messages.libraryScanFailed': + return ({required Object error}) => '无法扫描媒体库: ${error}'; + case 'messages.metadataRefreshing': + return ({required Object title}) => '正在刷新 “${title}” 的元数据...'; + case 'messages.metadataRefreshStarted': + return ({required Object title}) => '已开始刷新 “${title}” 的元数据'; + case 'messages.metadataRefreshFailed': + return ({required Object error}) => '无法刷新元数据: ${error}'; + case 'messages.noPlexToken': + return '未找到 Plex 令牌。请重新登录。'; + case 'messages.logoutConfirm': + return '你确定要登出吗?'; + case 'messages.noSeasonsFound': + return '未找到季'; + case 'messages.noEpisodesFound': + return '在第一季中未找到剧集'; + case 'messages.noEpisodesFoundGeneral': + return '未找到剧集'; + case 'messages.noResultsFound': + return '未找到结果'; + case 'messages.sleepTimerSet': + return ({required Object label}) => '睡眠定时器已设置为 ${label}'; + case 'messages.failedToSwitchProfile': + return ({required Object displayName}) => '无法切换到 ${displayName}'; + case 'messages.noItemsAvailable': + return '没有可用的项目'; + case 'messages.failedToCreatePlayQueue': + return '创建播放队列失败'; + case 'messages.failedToCreatePlayQueueNoItems': + return '创建播放队列失败 - 没有项目'; + case 'messages.failedPlayback': + return ({required Object action, required Object error}) => + '无法${action}: ${error}'; + case 'messages.youAreOffline': + return '您已离线'; + case 'messages.offlineFeatureUnavailable': + return '此功能需要网络连接。您的下载内容和设置仍然可用。'; + case 'profile.noUsersAvailable': + return '没有可用用户'; + case 'subtitlingStyling.stylingOptions': + return '样式选项'; + case 'subtitlingStyling.fontSize': + return '字号'; + case 'subtitlingStyling.textColor': + return '文本颜色'; + case 'subtitlingStyling.borderSize': + return '边框大小'; + case 'subtitlingStyling.borderColor': + return '边框颜色'; + case 'subtitlingStyling.backgroundOpacity': + return '背景不透明度'; + case 'subtitlingStyling.backgroundColor': + return '背景颜色'; + case 'dialog.confirmAction': + return '确认操作'; + case 'dialog.areYouSure': + return '确定要执行此操作吗?'; + case 'dialog.cancel': + return '取消'; + case 'dialog.playNow': + return '立即播放'; + case 'discover.title': + return '发现'; + case 'discover.switchProfile': + return '切换配置文件'; + case 'discover.switchServer': + return '切换服务器'; + case 'discover.logout': + return '登出'; + case 'discover.noContentAvailable': + return '没有可用内容'; + case 'discover.addMediaToLibraries': + return '请向你的媒体库添加一些媒体'; + case 'discover.continueWatching': + return '继续观看'; + case 'discover.recentlyAdded': + return '最近添加'; + case 'discover.play': + return '播放'; + case 'discover.resume': + return '继续'; + case 'discover.playEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.resumeEpisode': + return ({required Object season, required Object episode}) => + 'S${season}E${episode}'; + case 'discover.pause': + return '暂停'; + case 'discover.overview': + return '概述'; + case 'discover.cast': + return '演员表'; + case 'discover.seasons': + return '季数'; + case 'discover.studio': + return '制作公司'; + case 'discover.rating': + return '年龄分级'; + case 'discover.watched': + return '已观看'; + case 'discover.episodeCount': + return ({required Object count}) => '${count} 集'; + case 'discover.watchedProgress': + return ({required Object watched, required Object total}) => + '已观看 ${watched}/${total} 集'; + case 'discover.movie': + return '电影'; + case 'discover.tvShow': + return '电视剧'; + case 'discover.minutesLeft': + return ({required Object minutes}) => '剩余 ${minutes} 分钟'; + case 'errors.searchFailed': + return ({required Object error}) => '搜索失败: ${error}'; + case 'errors.connectionTimeout': + return ({required Object context}) => '加载 ${context} 时连接超时'; + case 'errors.connectionFailed': + return '无法连接到 Plex 服务器'; + case 'errors.failedToLoad': + return ({required Object context, required Object error}) => + '无法加载 ${context}: ${error}'; + case 'errors.noClientAvailable': + return '没有可用客户端'; + case 'errors.authenticationFailed': + return ({required Object error}) => '验证失败: ${error}'; + case 'errors.couldNotLaunchUrl': + return '无法打开授权 URL'; + case 'errors.pleaseEnterToken': + return '请输入一个令牌'; + case 'errors.invalidToken': + return '令牌无效'; + case 'errors.failedToVerifyToken': + return ({required Object error}) => '无法验证令牌: ${error}'; + case 'errors.failedToSwitchProfile': + return ({required Object displayName}) => '无法切换到 ${displayName}'; + case 'errors.connectionFailedGeneric': + return '连接失败'; + case 'libraries.title': + return '媒体库'; + case 'libraries.scanLibraryFiles': + return '扫描媒体库文件'; + case 'libraries.scanLibrary': + return '扫描媒体库'; + case 'libraries.analyze': + return '分析'; + case 'libraries.analyzeLibrary': + return '分析媒体库'; + case 'libraries.refreshMetadata': + return '刷新元数据'; + case 'libraries.emptyTrash': + return '清空回收站'; + case 'libraries.emptyingTrash': + return ({required Object title}) => '正在清空 “${title}” 的回收站...'; + case 'libraries.trashEmptied': + return ({required Object title}) => '已清空 “${title}” 的回收站'; + case 'libraries.failedToEmptyTrash': + return ({required Object error}) => '无法清空回收站: ${error}'; + case 'libraries.analyzing': + return ({required Object title}) => '正在分析 “${title}”...'; + case 'libraries.analysisStarted': + return ({required Object title}) => '已开始分析 “${title}”'; + case 'libraries.failedToAnalyze': + return ({required Object error}) => '无法分析媒体库: ${error}'; + case 'libraries.noLibrariesFound': + return '未找到媒体库'; + case 'libraries.thisLibraryIsEmpty': + return '此媒体库为空'; + case 'libraries.all': + return '全部'; + case 'libraries.clearAll': + return '全部清除'; + case 'libraries.scanLibraryConfirm': + return ({required Object title}) => '确定要扫描 “${title}” 吗?'; + case 'libraries.analyzeLibraryConfirm': + return ({required Object title}) => '确定要分析 “${title}” 吗?'; + case 'libraries.refreshMetadataConfirm': + return ({required Object title}) => '确定要刷新 “${title}” 的元数据吗?'; + case 'libraries.emptyTrashConfirm': + return ({required Object title}) => '确定要清空 “${title}” 的回收站吗?'; + case 'libraries.manageLibraries': + return '管理媒体库'; + case 'libraries.sort': + return '排序'; + case 'libraries.sortBy': + return '排序依据'; + case 'libraries.filters': + return '筛选器'; + case 'libraries.loadingLibraryWithCount': + return ({required Object count}) => '正在加载媒体库... (已加载 ${count} 个项目)'; + case 'libraries.confirmActionMessage': + return '确定要执行此操作吗?'; + case 'libraries.showLibrary': + return '显示媒体库'; + case 'libraries.hideLibrary': + return '隐藏媒体库'; + case 'libraries.libraryOptions': + return '媒体库选项'; + case 'libraries.content': + return '媒体库内容'; + case 'libraries.selectLibrary': + return '选择媒体库'; + case 'libraries.filtersWithCount': + return ({required Object count}) => '筛选器(${count})'; + case 'libraries.noRecommendations': + return '暂无推荐'; + case 'libraries.noCollections': + return '此媒体库中没有合集'; + case 'libraries.noFoldersFound': + return '未找到文件夹'; + case 'libraries.folders': + return '文件夹'; + case 'libraries.tabs.recommended': + return '推荐'; + case 'libraries.tabs.browse': + return '浏览'; + case 'libraries.tabs.collections': + return '合集'; + case 'libraries.tabs.playlists': + return '播放列表'; + case 'libraries.groupings.all': + return '全部'; + case 'libraries.groupings.movies': + return '电影'; + case 'libraries.groupings.shows': + return '剧集'; + case 'libraries.groupings.seasons': + return '季'; + case 'libraries.groupings.episodes': + return '集'; + case 'libraries.groupings.folders': + return '文件夹'; + case 'about.title': + return '关于'; + case 'about.openSourceLicenses': + return '开源许可证'; + case 'about.versionLabel': + return ({required Object version}) => '版本 ${version}'; + case 'about.appDescription': + return '一款精美的 Flutter Plex 客户端'; + case 'about.viewLicensesDescription': + return '查看第三方库的许可证'; + case 'serverSelection.connectingToServer': + return '正在连接服务器...'; + case 'serverSelection.connectingToAllServers': + return '正在连接所有服务器...'; + case 'serverSelection.connectToAllServers': + return '连接所有服务器'; + case 'serverSelection.allServerConnectionsFailed': + return '无法连接到任何服务器。请检查你的网络并重试。'; + case 'serverSelection.foundServers': + return ({required Object count}) => '找到 ${count} 个服务器'; + case 'serverSelection.orSelectIndividually': + return '或单独选择一个服务器:'; + case 'serverSelection.serverDebugCopied': + return '服务器调试数据已复制到剪贴板'; + case 'serverSelection.copyDebugData': + return '复制调试数据'; + case 'serverSelection.noServersFound': + return '未找到服务器'; + case 'serverSelection.noServersFoundForAccount': + return ({required Object username, required Object email}) => + '未找到 ${username} (${email}) 的服务器'; + case 'serverSelection.malformedServerData': + return ({required Object count}) => + '找到 ${count} 个数据格式错误的服务器。没有可用的有效服务器。'; + case 'serverSelection.incompleteServerInfo': + return '某些服务器信息不完整,已被跳过。请检查你的 Plex.tv 账户。'; + case 'serverSelection.incompleteConnectionInfo': + return '服务器连接信息不完整。请重试。'; + case 'serverSelection.malformedServerInfo': + return ({required Object message}) => '服务器信息格式错误: ${message}'; + case 'serverSelection.networkConnectionFailed': + return '网络连接失败。请检查你的网络连接并重试。'; + case 'serverSelection.authenticationFailed': + return '验证失败。请重新登录。'; + case 'serverSelection.plexServiceUnavailable': + return 'Plex 服务不可用。请稍后重试。'; + case 'serverSelection.failedToLoadServers': + return ({required Object error}) => '无法加载服务器: ${error}'; + case 'hubDetail.title': + return '标题'; + case 'hubDetail.releaseYear': + return '发行年份'; + case 'hubDetail.dateAdded': + return '添加日期'; + case 'hubDetail.rating': + return '评分'; + case 'hubDetail.noItemsFound': + return '未找到项目'; + case 'logs.title': + return '日志'; + case 'logs.clearLogs': + return '清除日志'; + case 'logs.copyLogs': + return '复制日志'; + case 'logs.exportLogs': + return '导出日志'; + case 'logs.noLogsToShow': + return '没有可显示的日志'; + case 'logs.error': + return '错误:'; + case 'logs.stackTrace': + return '堆栈跟踪 (Stack Trace):'; + case 'licenses.relatedPackages': + return '相关软件包'; + case 'licenses.license': + return '许可证'; + case 'licenses.licenseNumber': + return ({required Object number}) => '许可证 ${number}'; + case 'licenses.licensesCount': + return ({required Object count}) => '${count} 个许可证'; + case 'navigation.home': + return '主页'; + case 'navigation.search': + return '搜索'; + case 'navigation.libraries': + return '媒体库'; + case 'navigation.settings': + return '设置'; + case 'navigation.downloads': + return '下载'; + case 'downloads.title': + return '下载'; + case 'downloads.manage': + return '管理'; + case 'downloads.tvShows': + return '电视剧'; + case 'downloads.movies': + return '电影'; + case 'downloads.queue': + return '队列'; + case 'downloads.noDownloads': + return '暂无下载'; + case 'downloads.noDownloadsDescription': + return '下载的内容将在此处显示以供离线观看'; + case 'downloads.browseContent': + return '浏览内容'; + case 'downloads.downloading': + return '下载中'; + case 'downloads.downloaded': + return '已下载'; + case 'downloads.paused': + return '已暂停'; + case 'downloads.failed': + return '失败'; + case 'downloads.queued': + return '排队中'; + case 'downloads.downloadNow': + return '下载'; + case 'downloads.deleteDownload': + return '删除下载'; + case 'downloads.cancelDownload': + return '取消下载'; + case 'downloads.pauseDownload': + return '暂停下载'; + case 'downloads.resumeDownload': + return '恢复下载'; + case 'downloads.retryDownload': + return '重试下载'; + case 'downloads.downloadComplete': + return '下载完成'; + case 'downloads.downloadFailed': + return '下载失败'; + case 'downloads.downloadCancelled': + return '下载已取消'; + case 'downloads.downloadQueued': + return '下载已排队'; + case 'downloads.episodesQueued': + return ({required Object count}) => '${count} 集已加入下载队列'; + case 'downloads.downloadDeleted': + return '下载已删除'; + case 'downloads.deleteConfirmTitle': + return '删除下载?'; + case 'downloads.deleteConfirm': + return ({required Object title}) => + '确定要删除 "${title}" 吗?下载的文件将从您的设备中删除。'; + case 'downloads.deleteConfirmMessage': + return '下载的文件将从您的设备中删除。'; + case 'downloads.deletingWithProgress': + return ({ + required Object title, + required Object current, + required Object total, + }) => '正在删除 ${title}... (${current}/${total})'; + case 'downloads.deletingSimple': + return ({required Object title}) => '正在删除 ${title}...'; + case 'downloads.storageUsed': + return '已使用存储空间'; + case 'downloads.availableOffline': + return '可离线使用'; + case 'downloads.downloadingSeason': + return '正在下载季'; + case 'downloads.downloadingEpisode': + return '正在下载剧集'; + case 'offline.youAreOffline': + return '您已离线'; + case 'offline.offlineMode': + return '离线模式'; + case 'offline.backOnline': + return '已恢复在线'; + case 'offline.showingDownloadedContent': + return '仅显示已下载的内容'; + case 'offline.noInternetConnection': + return '无网络连接'; + case 'offline.serversUnreachable': + return '无法连接到 Plex 服务器'; + case 'offline.connectToDownload': + return '连接网络以下载内容'; + case 'playlists.title': + return '播放列表'; + case 'playlists.noPlaylists': + return '未找到播放列表'; + case 'playlists.create': + return '创建播放列表'; + case 'playlists.newPlaylist': + return '新播放列表'; + case 'playlists.playlistName': + return '播放列表名称'; + case 'playlists.enterPlaylistName': + return '输入播放列表名称'; + case 'playlists.edit': + return '编辑播放列表'; + case 'playlists.delete': + return '删除播放列表'; + case 'playlists.addTo': + return '添加到播放列表'; + case 'playlists.addItems': + return '添加项目'; + case 'playlists.removeItem': + return '从播放列表中移除'; + case 'playlists.clearPlaylist': + return '清空播放列表'; + case 'playlists.playAll': + return '全部播放'; + case 'playlists.shuffle': + return '随机播放'; + case 'playlists.smartPlaylist': + return '智能播放列表'; + case 'playlists.regularPlaylist': + return '普通播放列表'; + case 'playlists.itemCount': + return ({required Object count}) => '${count} 个项目'; + case 'playlists.oneItem': + return '1 个项目'; + case 'playlists.emptyPlaylist': + return '此播放列表为空'; + case 'playlists.deleteConfirm': + return '删除播放列表?'; + case 'playlists.deleteMessage': + return ({required Object name}) => '确定要删除 "${name}" 吗?'; + case 'playlists.created': + return '播放列表已创建'; + case 'playlists.updated': + return '播放列表已更新'; + case 'playlists.deleted': + return '播放列表已删除'; + case 'playlists.itemAdded': + return '已添加到播放列表'; + case 'playlists.itemRemoved': + return '已从播放列表中移除'; + case 'playlists.selectPlaylist': + return '选择播放列表'; + case 'playlists.createNewPlaylist': + return '创建新播放列表'; + case 'playlists.errorCreating': + return '创建播放列表失败'; + case 'playlists.errorDeleting': + return '删除播放列表失败'; + case 'playlists.errorLoading': + return '加载播放列表失败'; + case 'playlists.errorAdding': + return '添加到播放列表失败'; + case 'playlists.errorReordering': + return '重新排序播放列表项目失败'; + case 'playlists.errorRemoving': + return '从播放列表中移除失败'; + case 'playlists.playlist': + return '播放列表'; + case 'collections.title': + return '合集'; + case 'collections.collection': + return '合集'; + case 'collections.empty': + return '合集为空'; + case 'collections.noItems': + return '此合集没有项目'; + case 'collections.unknownLibrarySection': + return '无法删除:未知的媒体库分区'; + case 'collections.deleteCollection': + return '删除合集'; + case 'collections.deleteConfirm': + return ({required Object title}) => '确定要删除"${title}"吗?此操作无法撤销。'; + case 'collections.deleted': + return '已删除合集'; + case 'collections.deleteFailed': + return '删除合集失败'; + case 'collections.deleteFailedWithError': + return ({required Object error}) => '删除合集失败:${error}'; + case 'collections.failedToLoadItems': + return ({required Object error}) => '加载合集项目失败:${error}'; + case 'collections.addTo': + return '添加到合集'; + case 'collections.selectCollection': + return '选择合集'; + case 'collections.createNewCollection': + return '创建新合集'; + case 'collections.collectionName': + return '合集名称'; + case 'collections.enterCollectionName': + return '输入合集名称'; + case 'collections.addedToCollection': + return '已添加到合集'; + case 'collections.errorAddingToCollection': + return '添加到合集失败'; + case 'collections.created': + return '已创建合集'; + case 'collections.removeFromCollection': + return '从合集移除'; + case 'collections.removeFromCollectionConfirm': + return ({required Object title}) => '将“${title}”从此合集移除?'; + case 'collections.removedFromCollection': + return '已从合集移除'; + case 'collections.removeFromCollectionFailed': + return '从合集移除失败'; + case 'collections.removeFromCollectionError': + return ({required Object error}) => '从合集移除时出错:${error}'; + default: + return null; + } + } } diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index 8324e53f..05507197 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -139,7 +139,22 @@ "autoSkipCredits": "Auto Skip Credits", "autoSkipCreditsDescription": "Automatically skip credits and play next episode", "autoSkipDelay": "Auto Skip Delay", - "autoSkipDelayDescription": "Wait ${seconds} seconds before auto-skipping" + "autoSkipDelayDescription": "Wait ${seconds} seconds before auto-skipping", + "downloads": "Downloads", + "downloadLocationDescription": "Choose where to store downloaded content", + "downloadLocationDefault": "Default (App Storage)", + "downloadLocationCustom": "Custom Location", + "selectFolder": "Select Folder", + "resetToDefault": "Reset to Default", + "currentPath": "Current: ${path}", + "downloadLocationNotSupported": "Custom download location is not supported on iOS", + "downloadLocationChanged": "Download location changed", + "downloadLocationReset": "Download location reset to default", + "downloadLocationInvalid": "Selected folder is not writable", + "downloadLocationSelectError": "Failed to select folder", + "downloadOnWifiOnly": "Download on WiFi only", + "downloadOnWifiOnlyDescription": "Prevent downloads when on cellular data", + "cellularDownloadBlocked": "Downloads are disabled on cellular data. Connect to WiFi or change the setting." }, "search": { "hint": "Search movies, shows, music...", @@ -256,6 +271,8 @@ "messages": { "markedAsWatched": "Marked as watched", "markedAsUnwatched": "Marked as unwatched", + "markedAsWatchedOffline": "Marked as watched (will sync when online)", + "markedAsUnwatchedOffline": "Marked as unwatched (will sync when online)", "removedFromContinueWatching": "Removed from Continue Watching", "errorLoading": "Error: ${error}", "fileInfoNotAvailable": "File information not available", @@ -283,7 +300,9 @@ "noItemsAvailable": "No items available", "failedToCreatePlayQueue": "Failed to create play queue", "failedToCreatePlayQueueNoItems": "Failed to create play queue - no items", - "failedPlayback": "Failed to ${action}: ${error}" + "failedPlayback": "Failed to ${action}: ${error}", + "youAreOffline": "You're Offline", + "offlineFeatureUnavailable": "This feature requires an internet connection. Your downloads and settings are still available." }, "profile": { "noUsersAvailable": "No users available" @@ -313,8 +332,8 @@ "recentlyAdded": "Recently Added", "play": "Play", "resume": "Resume", - "playEpisode": "Play S${season}, E${episode}", - "resumeEpisode": "Resume S${season}, E${episode}", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "Pause", "overview": "Overview", "cast": "Cast", @@ -448,7 +467,8 @@ "home": "Home", "search": "Search", "libraries": "Libraries", - "settings": "Settings" + "settings": "Settings", + "downloads": "Downloads" }, "collections": { "title": "Collections", @@ -512,5 +532,50 @@ "errorAdding": "Failed to add to playlist", "errorReordering": "Failed to reorder playlist item", "errorRemoving": "Failed to remove from playlist" + }, + "downloads": { + "title": "Downloads", + "manage": "Manage", + "tvShows": "TV Shows", + "movies": "Movies", + "queue": "Queue", + "noDownloads": "No downloads yet", + "noDownloadsDescription": "Downloaded content will appear here for offline viewing", + "browseContent": "Browse content", + "downloading": "Downloading", + "downloaded": "Downloaded", + "paused": "Paused", + "failed": "Failed", + "queued": "Queued", + "downloadNow": "Download", + "deleteDownload": "Delete download", + "cancelDownload": "Cancel download", + "pauseDownload": "Pause download", + "resumeDownload": "Resume download", + "retryDownload": "Retry download", + "downloadComplete": "Download complete", + "downloadFailed": "Download failed", + "downloadCancelled": "Download cancelled", + "downloadQueued": "Download queued", + "episodesQueued": "${count} episodes queued for download", + "downloadDeleted": "Download deleted", + "deleteConfirmTitle": "Delete download?", + "deleteConfirm": "Are you sure you want to delete \"${title}\"? This will remove the downloaded file from your device.", + "deleteConfirmMessage": "This will remove the downloaded file from your device.", + "deletingWithProgress": "Deleting ${title}... (${current} of ${total})", + "deletingSimple": "Deleting ${title}...", + "storageUsed": "Storage used", + "availableOffline": "Available offline", + "downloadingSeason": "Downloading season", + "downloadingEpisode": "Downloading episode" + }, + "offline": { + "youAreOffline": "You're offline", + "offlineMode": "Offline mode", + "backOnline": "Back online", + "showingDownloadedContent": "Showing downloaded content only", + "noInternetConnection": "No internet connection", + "serversUnreachable": "Plex servers unreachable", + "connectToDownload": "Connect to the internet to download content" } } diff --git a/lib/i18n/strings_de.i18n.json b/lib/i18n/strings_de.i18n.json index 3b5afef9..1ca646f0 100644 --- a/lib/i18n/strings_de.i18n.json +++ b/lib/i18n/strings_de.i18n.json @@ -140,7 +140,22 @@ "autoSkipCredits": "Abspann automatisch überspringen", "autoSkipCreditsDescription": "Abspann automatisch überspringen und nächste Episode abspielen", "autoSkipDelay": "Verzögerung für automatisches Überspringen", - "autoSkipDelayDescription": "${seconds} Sekunden vor dem automatischen Überspringen warten" + "autoSkipDelayDescription": "${seconds} Sekunden vor dem automatischen Überspringen warten", + "downloads": "Downloads", + "downloadLocationDescription": "Speicherort für heruntergeladene Inhalte wählen", + "downloadLocationDefault": "Standard (App-Speicher)", + "downloadLocationCustom": "Benutzerdefinierter Speicherort", + "selectFolder": "Ordner auswählen", + "resetToDefault": "Auf Standard zurücksetzen", + "currentPath": "Aktuell: ${path}", + "downloadLocationNotSupported": "Benutzerdefinierter Download-Speicherort wird auf iOS nicht unterstützt", + "downloadLocationChanged": "Download-Speicherort geändert", + "downloadLocationReset": "Download-Speicherort auf Standard zurückgesetzt", + "downloadLocationInvalid": "Ausgewählter Ordner ist nicht beschreibbar", + "downloadLocationSelectError": "Ordnerauswahl fehlgeschlagen", + "downloadOnWifiOnly": "Nur über WLAN herunterladen", + "downloadOnWifiOnlyDescription": "Downloads über mobile Daten verhindern", + "cellularDownloadBlocked": "Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung." }, "search": { "hint": "Filme, Serien, Musik suchen...", @@ -257,7 +272,9 @@ "messages": { "markedAsWatched": "Als gesehen markiert", "markedAsUnwatched": "Als ungesehen markiert", - "removedFromContinueWatching": "Aus ‚Weiterschauen‘ entfernt", + "markedAsWatchedOffline": "Als gesehen markiert (wird synchronisiert, wenn online)", + "markedAsUnwatchedOffline": "Als ungesehen markiert (wird synchronisiert, wenn online)", + "removedFromContinueWatching": "Aus ‚Weiterschauen' entfernt", "errorLoading": "Fehler: ${error}", "fileInfoNotAvailable": "Dateiinfo nicht verfügbar", "errorLoadingFileInfo": "Fehler beim Laden der Dateiinfo: ${error}", @@ -284,7 +301,9 @@ "noItemsAvailable": "Keine Elemente verfügbar", "failedToCreatePlayQueue": "Wiedergabewarteschlange konnte nicht erstellt werden", "failedToCreatePlayQueueNoItems": "Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente", - "failedPlayback": "Wiedergabe für ${action} fehlgeschlagen: ${error}" + "failedPlayback": "Wiedergabe für ${action} fehlgeschlagen: ${error}", + "youAreOffline": "Du bist offline", + "offlineFeatureUnavailable": "Diese Funktion erfordert eine Internetverbindung. Deine Downloads und Einstellungen sind weiterhin verfügbar." }, "profile": { "noUsersAvailable": "Keine Benutzer verfügbar" @@ -315,8 +334,8 @@ "recentlyAdded": "Kürzlich hinzugefügt", "play": "Abspielen", "resume": "Fortsetzen", - "playEpisode": "S${season}, E${episode} - Abspielen", - "resumeEpisode": "S${season}, E${episode} - Fortsetzen", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "Pause", "overview": "Übersicht", "cast": "Besetzung", @@ -450,7 +469,53 @@ "home": "Start", "search": "Suche", "libraries": "Mediatheken", - "settings": "Einstellungen" + "settings": "Einstellungen", + "downloads": "Downloads" + }, + "downloads": { + "title": "Downloads", + "manage": "Verwalten", + "tvShows": "Serien", + "movies": "Filme", + "queue": "Warteschlange", + "noDownloads": "Noch keine Downloads", + "noDownloadsDescription": "Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt", + "browseContent": "Inhalte durchsuchen", + "downloading": "Wird heruntergeladen", + "downloaded": "Heruntergeladen", + "paused": "Pausiert", + "failed": "Fehlgeschlagen", + "queued": "In Warteschlange", + "downloadNow": "Herunterladen", + "deleteDownload": "Download löschen", + "cancelDownload": "Download abbrechen", + "pauseDownload": "Download pausieren", + "resumeDownload": "Download fortsetzen", + "retryDownload": "Download wiederholen", + "downloadComplete": "Download abgeschlossen", + "downloadFailed": "Download fehlgeschlagen", + "downloadCancelled": "Download abgebrochen", + "downloadQueued": "Download in Warteschlange", + "episodesQueued": "${count} Episoden zum Download hinzugefügt", + "downloadDeleted": "Download gelöscht", + "deleteConfirmTitle": "Download löschen?", + "deleteConfirm": "Möchtest du \"${title}\" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.", + "deleteConfirmMessage": "Die heruntergeladene Datei wird von deinem Gerät entfernt.", + "deletingWithProgress": "Lösche ${title}... (${current} von ${total})", + "deletingSimple": "Lösche ${title}...", + "storageUsed": "Speicherplatz belegt", + "availableOffline": "Offline verfügbar", + "downloadingSeason": "Staffel wird heruntergeladen", + "downloadingEpisode": "Episode wird heruntergeladen" + }, + "offline": { + "youAreOffline": "Du bist offline", + "offlineMode": "Offline-Modus", + "backOnline": "Wieder online", + "showingDownloadedContent": "Nur heruntergeladene Inhalte werden angezeigt", + "noInternetConnection": "Keine Internetverbindung", + "serversUnreachable": "Plex-Server nicht erreichbar", + "connectToDownload": "Verbinde dich mit dem Internet, um Inhalte herunterzuladen" }, "playlists": { "title": "Wiedergabelisten", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index 8aaec3fc..9d4483c6 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -140,7 +140,22 @@ "autoSkipCredits": "Salta Crediti Automaticamente", "autoSkipCreditsDescription": "Salta automaticamente i crediti e riproduci l'episodio successivo", "autoSkipDelay": "Ritardo Salto Automatico", - "autoSkipDelayDescription": "Aspetta ${seconds} secondi prima del salto automatico" + "autoSkipDelayDescription": "Aspetta ${seconds} secondi prima del salto automatico", + "downloads": "Download", + "downloadLocationDescription": "Scegli dove salvare i contenuti scaricati", + "downloadLocationDefault": "Predefinita (Archiviazione App)", + "downloadLocationCustom": "Posizione Personalizzata", + "selectFolder": "Seleziona Cartella", + "resetToDefault": "Ripristina Predefinita", + "currentPath": "Corrente: ${path}", + "downloadLocationNotSupported": "La posizione di download personalizzata non è supportata su iOS", + "downloadLocationChanged": "Posizione di download modificata", + "downloadLocationReset": "Posizione di download ripristinata a predefinita", + "downloadLocationInvalid": "La cartella selezionata non è scrivibile", + "downloadLocationSelectError": "Impossibile selezionare la cartella", + "downloadOnWifiOnly": "Scarica solo con WiFi", + "downloadOnWifiOnlyDescription": "Impedisci i download quando si utilizza la rete dati cellulare", + "cellularDownloadBlocked": "I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l'impostazione." }, "search": { "hint": "Cerca film. spettacoli, musica...", @@ -257,6 +272,8 @@ "messages": { "markedAsWatched": "Segna come visto", "markedAsUnwatched": "Segna come non visto", + "markedAsWatchedOffline": "Segnato come visto (sincronizzato online)", + "markedAsUnwatchedOffline": "Segnato come non visto (sincronizzato online)", "removedFromContinueWatching": "Rimosso da Continua a guardare", "errorLoading": "Errore: ${error}", "fileInfoNotAvailable": "Informazioni sul file non disponibili", @@ -284,7 +301,9 @@ "noItemsAvailable": "Nessun elemento disponibile", "failedToCreatePlayQueue": "Impossibile creare la coda di riproduzione", "failedToCreatePlayQueueNoItems": "Impossibile creare la coda di riproduzione - nessun elemento", - "failedPlayback": "Impossibile ${action}: ${error}" + "failedPlayback": "Impossibile ${action}: ${error}", + "youAreOffline": "Sei offline", + "offlineFeatureUnavailable": "Questa funzione richiede una connessione internet. I tuoi download e le impostazioni sono ancora disponibili." }, "profile": { "noUsersAvailable": "Nessun utente disponibile" @@ -315,8 +334,8 @@ "recentlyAdded": "Aggiunti di recente", "play": "Riproduci", "resume": "Riprendi", - "playEpisode": "Riproduci S${season}, E${episode}", - "resumeEpisode": "Riprendi S${season}, E${episode}", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "Pausa", "overview": "Panoramica", "cast": "Attori", @@ -450,7 +469,53 @@ "home": "Home", "search": "Cerca", "libraries": "Librerie", - "settings": "Impostazioni" + "settings": "Impostazioni", + "downloads": "Download" + }, + "downloads": { + "title": "Download", + "manage": "Gestisci", + "tvShows": "Serie TV", + "movies": "Film", + "queue": "Coda", + "noDownloads": "Nessun download", + "noDownloadsDescription": "I contenuti scaricati appariranno qui per la visualizzazione offline", + "browseContent": "Sfoglia contenuti", + "downloading": "Download in corso", + "downloaded": "Scaricato", + "paused": "In pausa", + "failed": "Fallito", + "queued": "In coda", + "downloadNow": "Scarica", + "deleteDownload": "Elimina download", + "cancelDownload": "Annulla download", + "pauseDownload": "Metti in pausa download", + "resumeDownload": "Riprendi download", + "retryDownload": "Riprova download", + "downloadComplete": "Download completato", + "downloadFailed": "Download fallito", + "downloadCancelled": "Download annullato", + "downloadQueued": "Download in coda", + "episodesQueued": "${count} episodi in coda per il download", + "downloadDeleted": "Download eliminato", + "deleteConfirmTitle": "Eliminare il download?", + "deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Il file scaricato verrà rimosso dal tuo dispositivo.", + "deleteConfirmMessage": "Il file scaricato verrà rimosso dal tuo dispositivo.", + "deletingWithProgress": "Eliminazione di ${title}... (${current} di ${total})", + "deletingSimple": "Eliminazione di ${title}...", + "storageUsed": "Spazio utilizzato", + "availableOffline": "Disponibile offline", + "downloadingSeason": "Download stagione in corso", + "downloadingEpisode": "Download episodio in corso" + }, + "offline": { + "youAreOffline": "Sei offline", + "offlineMode": "Modalità offline", + "backOnline": "Di nuovo online", + "showingDownloadedContent": "Visualizzazione solo contenuti scaricati", + "noInternetConnection": "Nessuna connessione internet", + "serversUnreachable": "Server Plex non raggiungibili", + "connectToDownload": "Connettiti a internet per scaricare contenuti" }, "playlists": { "title": "Playlist", diff --git a/lib/i18n/strings_nl.i18n.json b/lib/i18n/strings_nl.i18n.json index ce9c82c2..d17a98da 100644 --- a/lib/i18n/strings_nl.i18n.json +++ b/lib/i18n/strings_nl.i18n.json @@ -140,7 +140,22 @@ "autoSkipCredits": "Credits Automatisch Overslaan", "autoSkipCreditsDescription": "Credits automatisch overslaan en volgende aflevering afspelen", "autoSkipDelay": "Vertraging Automatisch Overslaan", - "autoSkipDelayDescription": "${seconds} seconden wachten voor automatisch overslaan" + "autoSkipDelayDescription": "${seconds} seconden wachten voor automatisch overslaan", + "downloads": "Downloads", + "downloadLocationDescription": "Kies waar gedownloade content wordt opgeslagen", + "downloadLocationDefault": "Standaard (App-opslag)", + "downloadLocationCustom": "Aangepaste Locatie", + "selectFolder": "Selecteer Map", + "resetToDefault": "Herstel naar Standaard", + "currentPath": "Huidig: ${path}", + "downloadLocationNotSupported": "Aangepaste downloadlocatie wordt niet ondersteund op iOS", + "downloadLocationChanged": "Downloadlocatie gewijzigd", + "downloadLocationReset": "Downloadlocatie hersteld naar standaard", + "downloadLocationInvalid": "Geselecteerde map is niet beschrijfbaar", + "downloadLocationSelectError": "Kan map niet selecteren", + "downloadOnWifiOnly": "Alleen via WiFi downloaden", + "downloadOnWifiOnlyDescription": "Voorkom downloads bij gebruik van mobiele data", + "cellularDownloadBlocked": "Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling." }, "search": { "hint": "Zoek films, series, muziek...", @@ -257,6 +272,8 @@ "messages": { "markedAsWatched": "Gemarkeerd als gekeken", "markedAsUnwatched": "Gemarkeerd als ongekeken", + "markedAsWatchedOffline": "Gemarkeerd als gekeken (sync wanneer online)", + "markedAsUnwatchedOffline": "Gemarkeerd als ongekeken (sync wanneer online)", "removedFromContinueWatching": "Verwijderd uit Doorgaan met kijken", "errorLoading": "Fout: ${error}", "fileInfoNotAvailable": "Bestand informatie niet beschikbaar", @@ -284,7 +301,9 @@ "noItemsAvailable": "Geen items beschikbaar", "failedToCreatePlayQueue": "Kan afspeelwachtrij niet maken", "failedToCreatePlayQueueNoItems": "Kan afspeelwachtrij niet maken - geen items", - "failedPlayback": "Afspelen van ${action} mislukt: ${error}" + "failedPlayback": "Afspelen van ${action} mislukt: ${error}", + "youAreOffline": "Je bent offline", + "offlineFeatureUnavailable": "Deze functie vereist een internetverbinding. Je downloads en instellingen zijn nog steeds beschikbaar." }, "profile": { "noUsersAvailable": "Geen gebruikers beschikbaar" @@ -315,8 +334,8 @@ "recentlyAdded": "Recent toegevoegd", "play": "Afspelen", "resume": "Hervatten", - "playEpisode": "Speel S${season}, E${episode}", - "resumeEpisode": "Hervat S${season}, E${episode}", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "Pauzeren", "overview": "Overzicht", "cast": "Acteurs", @@ -450,7 +469,53 @@ "home": "Thuis", "search": "Zoeken", "libraries": "Bibliotheken", - "settings": "Instellingen" + "settings": "Instellingen", + "downloads": "Downloads" + }, + "downloads": { + "title": "Downloads", + "manage": "Beheren", + "tvShows": "Series", + "movies": "Films", + "queue": "Wachtrij", + "noDownloads": "Nog geen downloads", + "noDownloadsDescription": "Gedownloade content verschijnt hier voor offline weergave", + "browseContent": "Blader door content", + "downloading": "Downloaden", + "downloaded": "Gedownload", + "paused": "Gepauzeerd", + "failed": "Mislukt", + "queued": "In wachtrij", + "downloadNow": "Download", + "deleteDownload": "Download verwijderen", + "cancelDownload": "Download annuleren", + "pauseDownload": "Download pauzeren", + "resumeDownload": "Download hervatten", + "retryDownload": "Download opnieuw proberen", + "downloadComplete": "Download voltooid", + "downloadFailed": "Download mislukt", + "downloadCancelled": "Download geannuleerd", + "downloadQueued": "Download in wachtrij", + "episodesQueued": "${count} afleveringen in wachtrij voor download", + "downloadDeleted": "Download verwijderd", + "deleteConfirmTitle": "Download verwijderen?", + "deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.", + "deleteConfirmMessage": "Het gedownloade bestand wordt van je apparaat verwijderd.", + "deletingWithProgress": "Verwijderen van ${title}... (${current} van ${total})", + "deletingSimple": "Verwijderen van ${title}...", + "storageUsed": "Opslag gebruikt", + "availableOffline": "Offline beschikbaar", + "downloadingSeason": "Seizoen downloaden", + "downloadingEpisode": "Aflevering downloaden" + }, + "offline": { + "youAreOffline": "Je bent offline", + "offlineMode": "Offline modus", + "backOnline": "Weer online", + "showingDownloadedContent": "Alleen gedownloade content wordt getoond", + "noInternetConnection": "Geen internetverbinding", + "serversUnreachable": "Plex servers onbereikbaar", + "connectToDownload": "Maak verbinding met internet om content te downloaden" }, "playlists": { "title": "Afspeellijsten", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index b8a2d184..4261366e 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -140,7 +140,22 @@ "autoSkipCredits": "Hoppa Över Credits Automatiskt", "autoSkipCreditsDescription": "Hoppa automatiskt över credits och spela nästa avsnitt", "autoSkipDelay": "Fördröjning Auto Hoppa Över", - "autoSkipDelayDescription": "Vänta ${seconds} sekunder innan automatisk överhoppning" + "autoSkipDelayDescription": "Vänta ${seconds} sekunder innan automatisk överhoppning", + "downloads": "Nedladdningar", + "downloadLocationDescription": "Välj var nedladdat innehåll ska lagras", + "downloadLocationDefault": "Standard (App-lagring)", + "downloadLocationCustom": "Anpassad Plats", + "selectFolder": "Välj Mapp", + "resetToDefault": "Återställ till Standard", + "currentPath": "Nuvarande: ${path}", + "downloadLocationNotSupported": "Anpassad nedladdningsplats stöds inte på iOS", + "downloadLocationChanged": "Nedladdningsplats ändrad", + "downloadLocationReset": "Nedladdningsplats återställd till standard", + "downloadLocationInvalid": "Vald mapp är inte skrivbar", + "downloadLocationSelectError": "Kunde inte välja mapp", + "downloadOnWifiOnly": "Ladda ner endast på WiFi", + "downloadOnWifiOnlyDescription": "Förhindra nedladdningar vid användning av mobildata", + "cellularDownloadBlocked": "Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen." }, "search": { "hint": "Sök filmer, serier, musik...", @@ -257,6 +272,8 @@ "messages": { "markedAsWatched": "Markerad som sedd", "markedAsUnwatched": "Markerad som osedd", + "markedAsWatchedOffline": "Markerad som sedd (synkroniseras när online)", + "markedAsUnwatchedOffline": "Markerad som osedd (synkroniseras när online)", "removedFromContinueWatching": "Borttagen från Fortsätt titta", "errorLoading": "Fel: ${error}", "fileInfoNotAvailable": "Filinformation inte tillgänglig", @@ -284,7 +301,9 @@ "noItemsAvailable": "Inga objekt tillgängliga", "failedToCreatePlayQueue": "Det gick inte att skapa uppspelningskö", "failedToCreatePlayQueueNoItems": "Det gick inte att skapa uppspelningskö – inga objekt", - "failedPlayback": "Kunde inte ${action}: ${error}" + "failedPlayback": "Kunde inte ${action}: ${error}", + "youAreOffline": "Du är offline", + "offlineFeatureUnavailable": "Denna funktion kräver en internetanslutning. Dina nedladdningar och inställningar är fortfarande tillgängliga." }, "profile": { "noUsersAvailable": "Inga användare tillgängliga" @@ -315,8 +334,8 @@ "recentlyAdded": "Nyligen tillagda", "play": "Spela", "resume": "Återuppta", - "playEpisode": "Spela S${season}, E${episode}", - "resumeEpisode": "Återuppta S${season}, E${episode}", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "Pausa", "overview": "Översikt", "cast": "Rollbesättning", @@ -450,7 +469,53 @@ "home": "Hem", "search": "Sök", "libraries": "Bibliotek", - "settings": "Inställningar" + "settings": "Inställningar", + "downloads": "Nedladdningar" + }, + "downloads": { + "title": "Nedladdningar", + "manage": "Hantera", + "tvShows": "TV-serier", + "movies": "Filmer", + "queue": "Kö", + "noDownloads": "Inga nedladdningar ännu", + "noDownloadsDescription": "Nedladdat innehåll visas här för offline-visning", + "browseContent": "Bläddra i innehåll", + "downloading": "Laddar ner", + "downloaded": "Nedladdad", + "paused": "Pausad", + "failed": "Misslyckades", + "queued": "I kö", + "downloadNow": "Ladda ner", + "deleteDownload": "Ta bort nedladdning", + "cancelDownload": "Avbryt nedladdning", + "pauseDownload": "Pausa nedladdning", + "resumeDownload": "Återuppta nedladdning", + "retryDownload": "Försök igen", + "downloadComplete": "Nedladdning klar", + "downloadFailed": "Nedladdning misslyckades", + "downloadCancelled": "Nedladdning avbruten", + "downloadQueued": "Nedladdning köad", + "episodesQueued": "${count} avsnitt köade för nedladdning", + "downloadDeleted": "Nedladdning borttagen", + "deleteConfirmTitle": "Ta bort nedladdning?", + "deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Den nedladdade filen kommer att tas bort från din enhet.", + "deleteConfirmMessage": "Den nedladdade filen kommer att tas bort från din enhet.", + "deletingWithProgress": "Tar bort ${title}... (${current} av ${total})", + "deletingSimple": "Tar bort ${title}...", + "storageUsed": "Lagring använd", + "availableOffline": "Tillgänglig offline", + "downloadingSeason": "Laddar ner säsong", + "downloadingEpisode": "Laddar ner avsnitt" + }, + "offline": { + "youAreOffline": "Du är offline", + "offlineMode": "Offline-läge", + "backOnline": "Tillbaka online", + "showingDownloadedContent": "Visar endast nedladdat innehåll", + "noInternetConnection": "Ingen internetanslutning", + "serversUnreachable": "Plex-servrar kan inte nås", + "connectToDownload": "Anslut till internet för att ladda ner innehåll" }, "playlists": { "title": "Spellistor", diff --git a/lib/i18n/strings_zh.i18n.json b/lib/i18n/strings_zh.i18n.json index fbe09427..5060fa17 100644 --- a/lib/i18n/strings_zh.i18n.json +++ b/lib/i18n/strings_zh.i18n.json @@ -140,7 +140,22 @@ "autoSkipCredits": "自动跳过片尾", "autoSkipCreditsDescription": "自动跳过片尾并播放下一集", "autoSkipDelay": "自动跳过延迟", - "autoSkipDelayDescription": "自动跳过前等待 ${seconds} 秒" + "autoSkipDelayDescription": "自动跳过前等待 ${seconds} 秒", + "downloads": "下载", + "downloadLocationDescription": "选择下载内容的存储位置", + "downloadLocationDefault": "默认(应用存储)", + "downloadLocationCustom": "自定义位置", + "selectFolder": "选择文件夹", + "resetToDefault": "重置为默认", + "currentPath": "当前: ${path}", + "downloadLocationNotSupported": "iOS 不支持自定义下载位置", + "downloadLocationChanged": "下载位置已更改", + "downloadLocationReset": "下载位置已重置为默认", + "downloadLocationInvalid": "所选文件夹不可写入", + "downloadLocationSelectError": "选择文件夹失败", + "downloadOnWifiOnly": "仅在 WiFi 时下载", + "downloadOnWifiOnlyDescription": "使用蜂窝数据时禁止下载", + "cellularDownloadBlocked": "蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。" }, "search": { "hint": "搜索电影、系列、音乐...", @@ -257,6 +272,8 @@ "messages": { "markedAsWatched": "已标记为已观看", "markedAsUnwatched": "已标记为未观看", + "markedAsWatchedOffline": "已标记为已观看 (将在联网时同步)", + "markedAsUnwatchedOffline": "已标记为未观看 (将在联网时同步)", "removedFromContinueWatching": "已从继续观看中移除", "errorLoading": "错误: ${error}", "fileInfoNotAvailable": "文件信息不可用", @@ -284,7 +301,9 @@ "noItemsAvailable": "没有可用的项目", "failedToCreatePlayQueue": "创建播放队列失败", "failedToCreatePlayQueueNoItems": "创建播放队列失败 - 没有项目", - "failedPlayback": "无法${action}: ${error}" + "failedPlayback": "无法${action}: ${error}", + "youAreOffline": "您已离线", + "offlineFeatureUnavailable": "此功能需要网络连接。您的下载内容和设置仍然可用。" }, "profile": { "noUsersAvailable": "没有可用用户" @@ -315,8 +334,8 @@ "recentlyAdded": "最近添加", "play": "播放", "resume": "继续", - "playEpisode": "播放 S${season}, E${episode}", - "resumeEpisode": "继续 S${season}, E${episode}", + "playEpisode": "S${season}E${episode}", + "resumeEpisode": "S${season}E${episode}", "pause": "暂停", "overview": "概述", "cast": "演员表", @@ -450,7 +469,53 @@ "home": "主页", "search": "搜索", "libraries": "媒体库", - "settings": "设置" + "settings": "设置", + "downloads": "下载" + }, + "downloads": { + "title": "下载", + "manage": "管理", + "tvShows": "电视剧", + "movies": "电影", + "queue": "队列", + "noDownloads": "暂无下载", + "noDownloadsDescription": "下载的内容将在此处显示以供离线观看", + "browseContent": "浏览内容", + "downloading": "下载中", + "downloaded": "已下载", + "paused": "已暂停", + "failed": "失败", + "queued": "排队中", + "downloadNow": "下载", + "deleteDownload": "删除下载", + "cancelDownload": "取消下载", + "pauseDownload": "暂停下载", + "resumeDownload": "恢复下载", + "retryDownload": "重试下载", + "downloadComplete": "下载完成", + "downloadFailed": "下载失败", + "downloadCancelled": "下载已取消", + "downloadQueued": "下载已排队", + "episodesQueued": "${count} 集已加入下载队列", + "downloadDeleted": "下载已删除", + "deleteConfirmTitle": "删除下载?", + "deleteConfirm": "确定要删除 \"${title}\" 吗?下载的文件将从您的设备中删除。", + "deleteConfirmMessage": "下载的文件将从您的设备中删除。", + "deletingWithProgress": "正在删除 ${title}... (${current}/${total})", + "deletingSimple": "正在删除 ${title}...", + "storageUsed": "已使用存储空间", + "availableOffline": "可离线使用", + "downloadingSeason": "正在下载季", + "downloadingEpisode": "正在下载剧集" + }, + "offline": { + "youAreOffline": "您已离线", + "offlineMode": "离线模式", + "backOnline": "已恢复在线", + "showingDownloadedContent": "仅显示已下载的内容", + "noInternetConnection": "无网络连接", + "serversUnreachable": "无法连接到 Plex 服务器", + "connectToDownload": "连接网络以下载内容" }, "playlists": { "title": "播放列表", diff --git a/lib/main.dart b/lib/main.dart index 8d057354..da52fbd4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,9 +20,17 @@ import 'providers/theme_provider.dart'; import 'providers/settings_provider.dart'; import 'providers/hidden_libraries_provider.dart'; import 'providers/playback_state_provider.dart'; +import 'providers/download_provider.dart'; +import 'providers/offline_mode_provider.dart'; +import 'providers/offline_watch_provider.dart'; import 'services/multi_server_manager.dart'; +import 'services/offline_watch_sync_service.dart'; import 'services/data_aggregation_service.dart'; import 'services/server_registry.dart'; +import 'services/download_manager_service.dart'; +import 'services/download_storage_service.dart'; +import 'services/plex_api_cache.dart'; +import 'database/app_database.dart'; import 'utils/app_logger.dart'; import 'utils/orientation_helper.dart'; import 'utils/language_codes.dart'; @@ -71,6 +79,9 @@ void main() async { final debugEnabled = settings.getEnableDebugLogging(); setLoggerLevel(debugEnabled); + // Initialize download storage service with settings + await DownloadStorageService.instance.initialize(settings); + // Start global fullscreen state monitoring FullscreenStateManager().startMonitoring(); @@ -87,15 +98,60 @@ void main() async { // Global RouteObserver for tracking navigation final RouteObserver routeObserver = RouteObserver(); -class MainApp extends StatelessWidget { +class MainApp extends StatefulWidget { const MainApp({super.key}); @override - Widget build(BuildContext context) { - // Initialize multi-server infrastructure - final serverManager = MultiServerManager(); - final aggregationService = DataAggregationService(serverManager); + State createState() => _MainAppState(); +} +class _MainAppState extends State with WidgetsBindingObserver { + // Initialize multi-server infrastructure + late final MultiServerManager _serverManager; + late final DataAggregationService _aggregationService; + late final AppDatabase _appDatabase; + late final DownloadManagerService _downloadManager; + late final OfflineWatchSyncService _offlineWatchSyncService; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + _serverManager = MultiServerManager(); + _aggregationService = DataAggregationService(_serverManager); + _appDatabase = AppDatabase(); + + // Initialize API cache with database + PlexApiCache.initialize(_appDatabase); + + _downloadManager = DownloadManagerService( + database: _appDatabase, + storageService: DownloadStorageService.instance, + ); + + _offlineWatchSyncService = OfflineWatchSyncService( + database: _appDatabase, + serverManager: _serverManager, + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // App came back to foreground - trigger sync check + _offlineWatchSyncService.onAppResumed(); + } + } + + @override + Widget build(BuildContext context) { return MultiProvider( providers: [ // Legacy provider for backward compatibility @@ -103,9 +159,60 @@ class MainApp extends StatelessWidget { // New multi-server providers ChangeNotifierProvider( create: (context) => - MultiServerProvider(serverManager, aggregationService), + MultiServerProvider(_serverManager, _aggregationService), ), ChangeNotifierProvider(create: (context) => ServerStateProvider()), + // Offline mode provider - depends on MultiServerProvider + ChangeNotifierProxyProvider( + create: (_) => OfflineModeProvider(_serverManager), + update: (_, multiServerProvider, previous) { + final provider = previous ?? OfflineModeProvider(_serverManager); + provider.initialize(); + return provider; + }, + ), + // Download provider + ChangeNotifierProvider( + create: (context) => + DownloadProvider(downloadManager: _downloadManager), + ), + // Offline watch sync service + ChangeNotifierProvider( + create: (context) { + final offlineModeProvider = context.read(); + final downloadProvider = context.read(); + + // Wire up callback to refresh download provider after watch state sync + _offlineWatchSyncService.onWatchStatesRefreshed = () { + downloadProvider.refreshMetadataFromCache(); + }; + + _offlineWatchSyncService.startConnectivityMonitoring( + offlineModeProvider, + ); + return _offlineWatchSyncService; + }, + ), + // Offline watch provider - depends on sync service and download provider + ChangeNotifierProxyProvider2< + OfflineWatchSyncService, + DownloadProvider, + OfflineWatchProvider + >( + create: (context) => OfflineWatchProvider( + syncService: _offlineWatchSyncService, + downloadProvider: context.read(), + apiCache: PlexApiCache.instance, + ), + update: (_, syncService, downloadProvider, previous) { + return previous ?? + OfflineWatchProvider( + syncService: syncService, + downloadProvider: downloadProvider, + apiCache: PlexApiCache.instance, + ); + }, + ), // Existing providers ChangeNotifierProvider(create: (context) => UserProfileProvider()), ChangeNotifierProvider(create: (context) => ThemeProvider()), @@ -307,6 +414,9 @@ class _SetupScreenState extends State { appLogger.i('Successfully connected to $connectedCount servers'); if (mounted) { + // Now that Plex clients are available, trigger initial watch sync + context.read().onServersConnected(); + // Navigate to main screen immediately // Get first connected client for backward compatibility final firstClient = @@ -323,14 +433,17 @@ class _SetupScreenState extends State { _checkForUpdatesOnStartup(); } } else { - // All connections failed - appLogger.w('Failed to connect to any servers'); + // All connections failed - navigate to offline mode + appLogger.w('Failed to connect to any servers, entering offline mode'); if (mounted) { - // Show auth screen to re-authenticate + // Navigate to MainScreen in offline mode + // User can still access Downloads and Settings Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const AuthScreen()), + MaterialPageRoute( + builder: (context) => const MainScreen(isOfflineMode: true), + ), ); } } @@ -342,9 +455,12 @@ class _SetupScreenState extends State { ); if (mounted) { + // Navigate to MainScreen in offline mode Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const AuthScreen()), + MaterialPageRoute( + builder: (context) => const MainScreen(isOfflineMode: true), + ), ); } } diff --git a/lib/models/deletion_progress.dart b/lib/models/deletion_progress.dart new file mode 100644 index 00000000..ac4d6c12 --- /dev/null +++ b/lib/models/deletion_progress.dart @@ -0,0 +1,45 @@ +class DeletionProgress { + final String globalKey; + final String itemTitle; + final int currentItem; + final int totalItems; + final String? currentOperation; + + const DeletionProgress({ + required this.globalKey, + required this.itemTitle, + required this.currentItem, + required this.totalItems, + this.currentOperation, + }); + + double get progressPercent => + totalItems > 0 ? (currentItem / totalItems) : 0.0; + + int get progressPercentInt => (progressPercent * 100).round(); + + bool get isComplete => currentItem >= totalItems; + + DeletionProgress copyWith({ + String? globalKey, + String? itemTitle, + int? currentItem, + int? totalItems, + String? currentOperation, + }) { + return DeletionProgress( + globalKey: globalKey ?? this.globalKey, + itemTitle: itemTitle ?? this.itemTitle, + currentItem: currentItem ?? this.currentItem, + totalItems: totalItems ?? this.totalItems, + currentOperation: currentOperation ?? this.currentOperation, + ); + } + + @override + String toString() { + return 'DeletionProgress(globalKey: $globalKey, itemTitle: $itemTitle, ' + 'currentItem: $currentItem, totalItems: $totalItems, ' + 'progressPercent: $progressPercentInt%)'; + } +} diff --git a/lib/models/download_progress.dart b/lib/models/download_progress.dart new file mode 100644 index 00000000..2d42e7ed --- /dev/null +++ b/lib/models/download_progress.dart @@ -0,0 +1,81 @@ +import 'download_status.dart'; + +class DownloadProgress { + final String globalKey; + final DownloadStatus status; + final int progress; // 0-100 + final int downloadedBytes; + final int totalBytes; + final double speed; // bytes per second + final String? errorMessage; + final String? + currentFile; // What's being downloaded (video, subtitles, artwork) + + // Thumbnail path (populated after artwork download completes) + final String? thumbPath; + + const DownloadProgress({ + required this.globalKey, + required this.status, + this.progress = 0, + this.downloadedBytes = 0, + this.totalBytes = 0, + this.speed = 0, + this.errorMessage, + this.currentFile, + this.thumbPath, + }); + + double get progressPercent => progress / 100.0; + + String get speedFormatted { + if (speed < 1024) return '${speed.toStringAsFixed(0)} B/s'; + if (speed < 1024 * 1024) return '${(speed / 1024).toStringAsFixed(1)} KB/s'; + return '${(speed / (1024 * 1024)).toStringAsFixed(1)} MB/s'; + } + + String get downloadedFormatted => _formatBytes(downloadedBytes); + String get totalFormatted => _formatBytes(totalBytes); + + String _formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + if (bytes < 1024 * 1024 * 1024) + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; + } + + Duration? get estimatedTimeRemaining { + if (speed <= 0 || totalBytes <= 0) return null; + final remainingBytes = totalBytes - downloadedBytes; + if (remainingBytes <= 0) return Duration.zero; + return Duration(seconds: (remainingBytes / speed).round()); + } + + /// Check if this progress update includes artwork paths + bool get hasArtworkPaths => thumbPath != null; + + DownloadProgress copyWith({ + String? globalKey, + DownloadStatus? status, + int? progress, + int? downloadedBytes, + int? totalBytes, + double? speed, + String? errorMessage, + String? currentFile, + String? thumbPath, + }) { + return DownloadProgress( + globalKey: globalKey ?? this.globalKey, + status: status ?? this.status, + progress: progress ?? this.progress, + downloadedBytes: downloadedBytes ?? this.downloadedBytes, + totalBytes: totalBytes ?? this.totalBytes, + speed: speed ?? this.speed, + errorMessage: errorMessage ?? this.errorMessage, + currentFile: currentFile ?? this.currentFile, + thumbPath: thumbPath ?? this.thumbPath, + ); + } +} diff --git a/lib/models/download_status.dart b/lib/models/download_status.dart new file mode 100644 index 00000000..f763de72 --- /dev/null +++ b/lib/models/download_status.dart @@ -0,0 +1,9 @@ +enum DownloadStatus { + queued, + downloading, + paused, + completed, + failed, + cancelled, + partial, // Some episodes downloaded, but not all (for shows/seasons) +} diff --git a/lib/models/plex_media_info.dart b/lib/models/plex_media_info.dart index 9981996c..03b6d42d 100644 --- a/lib/models/plex_media_info.dart +++ b/lib/models/plex_media_info.dart @@ -201,3 +201,11 @@ class PlexMarker { return posMs >= startTimeOffset && posMs <= endTimeOffset; } } + +/// Combined chapters and markers fetched in a single API call +class PlaybackExtras { + final List chapters; + final List markers; + + PlaybackExtras({required this.chapters, required this.markers}); +} diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index 54771818..34ba2beb 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -55,9 +55,8 @@ class PlexMetadata with MultiServerFields { @JsonKey(includeFromJson: false, includeToJson: false) final String? serverName; - // Transient field for clear logo (extracted from Image array) - String? _clearLogo; - String? get clearLogo => _clearLogo; + // Clear logo URL (extracted from Image array, but serialized for offline storage) + final String? clearLogo; /// Global unique identifier across all servers (serverId:ratingKey) String get globalKey => serverId != null ? '$serverId:$ratingKey' : ratingKey; @@ -103,6 +102,7 @@ class PlexMetadata with MultiServerFields { this.librarySectionID, this.serverId, this.serverName, + this.clearLogo, }); /// Create a copy of this metadata with optional field overrides @@ -147,8 +147,9 @@ class PlexMetadata with MultiServerFields { int? librarySectionID, String? serverId, String? serverName, + String? clearLogo, }) { - final copy = PlexMetadata( + return PlexMetadata( ratingKey: ratingKey ?? this.ratingKey, key: key ?? this.key, guid: guid ?? this.guid, @@ -189,32 +190,34 @@ class PlexMetadata with MultiServerFields { librarySectionID: librarySectionID ?? this.librarySectionID, serverId: serverId ?? this.serverId, serverName: serverName ?? this.serverName, + clearLogo: clearLogo ?? this.clearLogo, ); - // Preserve clearLogo - copy._clearLogo = _clearLogo; - return copy; } - // Extract clearLogo from Image array in raw JSON - void _extractClearLogo(Map json) { - if (!json.containsKey('Image')) return; + /// Extract clearLogo from Image array in raw JSON + static String? _extractClearLogoFromJson(Map json) { + if (!json.containsKey('Image')) return null; final images = json['Image'] as List?; - if (images == null) return; + if (images == null) return null; for (var image in images) { if (image is Map && image['type'] == 'clearLogo') { - _clearLogo = image['url'] as String?; - return; + return image['url'] as String?; } } + return null; } - // Custom factory that extracts clearLogo + /// Create from JSON with clearLogo extracted from Image array factory PlexMetadata.fromJsonWithImages(Map json) { - final metadata = PlexMetadata.fromJson(json); - metadata._extractClearLogo(json); - return metadata; + // Extract clearLogo before parsing + final clearLogoUrl = _extractClearLogoFromJson(json); + // Add it to the json so it gets parsed + if (clearLogoUrl != null) { + json['clearLogo'] = clearLogoUrl; + } + return PlexMetadata.fromJson(json); } // Helper to get the display title (show name for episodes/seasons, title otherwise) diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index 050b3ee2..a4dbc4d8 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -47,6 +47,7 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( playlistItemID: (json['playlistItemID'] as num?)?.toInt(), playQueueItemID: (json['playQueueItemID'] as num?)?.toInt(), librarySectionID: (json['librarySectionID'] as num?)?.toInt(), + clearLogo: json['clearLogo'] as String?, ); Map _$PlexMetadataToJson(PlexMetadata instance) => @@ -89,4 +90,5 @@ Map _$PlexMetadataToJson(PlexMetadata instance) => 'playlistItemID': instance.playlistItemID, 'playQueueItemID': instance.playQueueItemID, 'librarySectionID': instance.librarySectionID, + 'clearLogo': instance.clearLogo, }; diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart new file mode 100644 index 00000000..9fec10c3 --- /dev/null +++ b/lib/providers/download_provider.dart @@ -0,0 +1,1277 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:collection'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/download_status.dart'; +import '../models/download_progress.dart'; +import '../models/deletion_progress.dart'; +import '../models/plex_metadata.dart'; +import '../services/download_manager_service.dart'; +import '../services/download_storage_service.dart'; +import '../services/plex_api_cache.dart'; +import '../services/plex_client.dart'; +import '../utils/app_logger.dart'; + +/// Holds Plex thumb path reference for downloaded artwork. +/// The actual file path is computed from the hash of serverId + thumb path. +class DownloadedArtwork { + /// The Plex thumb path (e.g., /library/metadata/12345/thumb/1234567890) + final String? thumbPath; + + const DownloadedArtwork({this.thumbPath}); + + /// Get the local file path for this artwork + String? getLocalPath(DownloadStorageService storage, String serverId) { + if (thumbPath == null) return null; + return storage.getArtworkPathSync(serverId, thumbPath!); + } +} + +/// Provider for managing download state and operations. +class DownloadProvider extends ChangeNotifier { + final DownloadManagerService _downloadManager; + StreamSubscription? _progressSubscription; + StreamSubscription? _deletionProgressSubscription; + + // Track download progress by globalKey (serverId:ratingKey) + final Map _downloads = {}; + + // Store metadata for display + final Map _metadata = {}; + + // Store Plex thumb paths for offline display (actual file path computed from hash) + final Map _artworkPaths = {}; + + // Track items currently being queued (building download queue) + final Set _queueing = {}; + + // Track items currently being deleted with progress + final Map _deletionProgress = {}; + + // Track total episode counts for shows/seasons (for partial download detection) + // Key: globalKey (serverId:ratingKey), Value: total episode count + final Map _totalEpisodeCounts = {}; + + DownloadProvider({required DownloadManagerService downloadManager}) + : _downloadManager = downloadManager { + // Listen to progress updates from the download manager + _progressSubscription = _downloadManager.progressStream.listen( + _onProgressUpdate, + ); + + // Listen to deletion progress updates + _deletionProgressSubscription = _downloadManager.deletionProgressStream + .listen(_onDeletionProgressUpdate); + + // Load persisted downloads from database + _loadPersistedDownloads(); + } + + /// Load all persisted downloads and metadata from the database/cache + Future _loadPersistedDownloads() async { + try { + // Clear existing data to prevent stale entries after deletions + _downloads.clear(); + _artworkPaths.clear(); + _metadata.clear(); + _totalEpisodeCounts.clear(); + + final storageService = DownloadStorageService.instance; + final apiCache = PlexApiCache.instance; + + // Initialize artwork directory path for synchronous access + await storageService.getArtworkDirectory(); + + // Load all downloads from database + final downloads = await _downloadManager.getAllDownloads(); + for (final item in downloads) { + _downloads[item.globalKey] = DownloadProgress( + globalKey: item.globalKey, + status: DownloadStatus.values[item.status], + progress: item.progress, + downloadedBytes: item.downloadedBytes, + totalBytes: item.totalBytes ?? 0, + ); + + // Store Plex thumb path reference (file path computed from hash when needed) + _artworkPaths[item.globalKey] = DownloadedArtwork( + thumbPath: item.thumbPath, + ); + + // Load metadata from API cache (base endpoint - chapters/markers included in data) + final cached = await apiCache.get( + item.serverId, + '/library/metadata/${item.ratingKey}', + ); + if (cached != null) { + final metadataList = cached['MediaContainer']?['Metadata'] as List?; + if (metadataList != null && metadataList.isNotEmpty) { + final metadata = PlexMetadata.fromJson( + metadataList[0], + ).copyWith(serverId: item.serverId); + _metadata[item.globalKey] = metadata; + + // For episodes, also load parent (show and season) metadata + if (metadata.type == 'episode') { + await _loadParentMetadataFromCache(metadata, apiCache); + } + } + } + } + + // Load total episode counts from SharedPreferences + await _loadTotalEpisodeCounts(); + + appLogger.i( + 'Loaded ${_downloads.length} downloads, ${_metadata.length} metadata entries, ' + 'and ${_totalEpisodeCounts.length} episode counts', + ); + notifyListeners(); + } catch (e) { + appLogger.e('Failed to load persisted downloads', error: e); + } + } + + /// Load total episode counts from SharedPreferences + Future _loadTotalEpisodeCounts() async { + try { + final prefs = await SharedPreferences.getInstance(); + final keys = prefs.getKeys().where((k) => k.startsWith('episode_count_')); + + for (final key in keys) { + final globalKey = key.replaceFirst('episode_count_', ''); + final count = prefs.getInt(key); + if (count != null) { + _totalEpisodeCounts[globalKey] = count; + appLogger.d( + '📂 Loaded episode count from SharedPrefs: $globalKey = $count', + ); + } + } + + appLogger.i( + '📚 Loaded ${_totalEpisodeCounts.length} episode counts from SharedPreferences', + ); + } catch (e) { + appLogger.w('Failed to load episode counts', error: e); + } + } + + /// Persist total episode count to SharedPreferences + Future _persistTotalEpisodeCount(String globalKey, int count) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('episode_count_$globalKey', count); + appLogger.d('Persisted episode count for $globalKey: $count'); + } catch (e) { + appLogger.w('Failed to persist episode count for $globalKey', error: e); + } + } + + /// Load parent (show and season) metadata from cache for an episode + Future _loadParentMetadataFromCache( + PlexMetadata episode, + PlexApiCache apiCache, + ) async { + final serverId = episode.serverId; + if (serverId == null) return; + + // Load show metadata (base endpoint) + final showRatingKey = episode.grandparentRatingKey; + if (showRatingKey != null) { + final showGlobalKey = '$serverId:$showRatingKey'; + if (!_metadata.containsKey(showGlobalKey)) { + final cached = await apiCache.get( + serverId, + '/library/metadata/$showRatingKey', + ); + if (cached != null) { + final metadataList = cached['MediaContainer']?['Metadata'] as List?; + if (metadataList != null && metadataList.isNotEmpty) { + final showMetadata = PlexMetadata.fromJson( + metadataList[0], + ).copyWith(serverId: serverId); + _metadata[showGlobalKey] = showMetadata; + // Store artwork reference for offline display + if (showMetadata.thumb != null) { + _artworkPaths[showGlobalKey] = DownloadedArtwork( + thumbPath: showMetadata.thumb, + ); + } + } + } + } + } + + // Load season metadata (base endpoint) + final seasonRatingKey = episode.parentRatingKey; + if (seasonRatingKey != null) { + final seasonGlobalKey = '$serverId:$seasonRatingKey'; + if (!_metadata.containsKey(seasonGlobalKey)) { + final cached = await apiCache.get( + serverId, + '/library/metadata/$seasonRatingKey', + ); + if (cached != null) { + final metadataList = cached['MediaContainer']?['Metadata'] as List?; + if (metadataList != null && metadataList.isNotEmpty) { + final seasonMetadata = PlexMetadata.fromJson( + metadataList[0], + ).copyWith(serverId: serverId); + _metadata[seasonGlobalKey] = seasonMetadata; + // Store artwork reference for offline display + if (seasonMetadata.thumb != null) { + _artworkPaths[seasonGlobalKey] = DownloadedArtwork( + thumbPath: seasonMetadata.thumb, + ); + } + } + } + } + } + } + + void _onProgressUpdate(DownloadProgress progress) { + appLogger.d( + 'Progress update received: ${progress.globalKey} - ${progress.status} - ${progress.progress}%', + ); + + _downloads[progress.globalKey] = progress; + + // Sync artwork paths when they are available + if (progress.hasArtworkPaths) { + _artworkPaths[progress.globalKey] = DownloadedArtwork( + thumbPath: progress.thumbPath, + ); + } + + appLogger.d('Notifying listeners for ${progress.globalKey}'); + notifyListeners(); + } + + @override + @override + void dispose() { + _progressSubscription?.cancel(); + _deletionProgressSubscription?.cancel(); + super.dispose(); + } + + /// All current download progress entries + Map get downloads => Map.unmodifiable(_downloads); + + /// All metadata for downloads + Map get metadata => Map.unmodifiable(_metadata); + + /// Get all queued/downloading items (for Queue tab) + List get queuedDownloads { + return _downloads.values + .where( + (p) => + p.status == DownloadStatus.queued || + p.status == DownloadStatus.downloading || + p.status == DownloadStatus.paused, + ) + .toList(); + } + + /// Get all completed downloads + List get completedDownloads { + return _downloads.values + .where((p) => p.status == DownloadStatus.completed) + .toList(); + } + + /// Get completed TV episode downloads (individual episodes) + List get downloadedEpisodes { + return _metadata.entries + .where((entry) { + final progress = _downloads[entry.key]; + return progress?.status == DownloadStatus.completed && + entry.value.type == 'episode'; + }) + .map((entry) => entry.value) + .toList(); + } + + /// Get unique TV shows that have downloaded episodes + /// Returns stored show metadata, or synthesizes from episode metadata as fallback + List get downloadedShows { + final Map shows = {}; + + for (final entry in _metadata.entries) { + final globalKey = entry.key; + final meta = entry.value; + final progress = _downloads[globalKey]; + + if (progress?.status == DownloadStatus.completed && + meta.type == 'episode') { + final showRatingKey = meta.grandparentRatingKey; + if (showRatingKey != null && !shows.containsKey(showRatingKey)) { + // Try to get stored show metadata first + final showGlobalKey = '${meta.serverId}:$showRatingKey'; + final storedShow = _metadata[showGlobalKey]; + + if (storedShow != null && storedShow.type == 'show') { + // Use stored show metadata (has year, summary, clearLogo) + shows[showRatingKey] = storedShow; + } else { + // Fallback: synthesize from episode metadata (missing year, summary) + shows[showRatingKey] = PlexMetadata( + ratingKey: showRatingKey, + key: '/library/metadata/$showRatingKey', + type: 'show', + title: meta.grandparentTitle ?? 'Unknown Show', + thumb: meta.grandparentThumb, + art: meta.grandparentArt, + serverId: meta.serverId, + ); + } + } + } + } + + return shows.values.toList(); + } + + /// Get completed movie downloads + List get downloadedMovies { + return _metadata.entries + .where((entry) { + final progress = _downloads[entry.key]; + return progress?.status == DownloadStatus.completed && + entry.value.type == 'movie'; + }) + .map((entry) => entry.value) + .toList(); + } + + /// Get metadata for a specific download + PlexMetadata? getMetadata(String globalKey) => _metadata[globalKey]; + + /// Get artwork paths for a specific download (for offline display) + DownloadedArtwork? getArtworkPaths(String globalKey) => + _artworkPaths[globalKey]; + + /// Get local file path for any artwork type (thumb, art, clearLogo, etc.) + /// Returns null if artwork directory isn't initialized or artworkPath is null + String? getArtworkLocalPath(String serverId, String? artworkPath) { + if (artworkPath == null) return null; + return DownloadStorageService.instance.getArtworkPathSync( + serverId, + artworkPath, + ); + } + + /// Get downloaded episodes for a specific show (by grandparentRatingKey) + List getDownloadedEpisodesForShow(String showRatingKey) { + return _metadata.entries + .where((entry) { + final progress = _downloads[entry.key]; + final meta = entry.value; + return progress?.status == DownloadStatus.completed && + meta.type == 'episode' && + meta.grandparentRatingKey == showRatingKey; + }) + .map((entry) => entry.value) + .toList(); + } + + /// Get all episode downloads (any status) for a specific show + List _getEpisodeDownloadsForShow(String showRatingKey) { + return _downloads.entries + .where((entry) { + final meta = _metadata[entry.key]; + return meta?.type == 'episode' && + meta?.grandparentRatingKey == showRatingKey; + }) + .map((entry) => entry.value) + .toList(); + } + + /// Get all episode downloads (any status) for a specific season + List _getEpisodeDownloadsForSeason(String seasonRatingKey) { + return _downloads.entries + .where((entry) { + final meta = _metadata[entry.key]; + return meta?.type == 'episode' && + meta?.parentRatingKey == seasonRatingKey; + }) + .map((entry) => entry.value) + .toList(); + } + + /// Calculate aggregate progress for a show (based on all its episodes) + /// Returns synthetic DownloadProgress with aggregated values + DownloadProgress? getAggregateProgressForShow( + String serverId, + String showRatingKey, + ) { + final globalKey = '$serverId:$showRatingKey'; + final episodes = _getEpisodeDownloadsForShow(showRatingKey); + + // DIAGNOSTIC: Check all sources of episode count + final showMeta = _metadata[globalKey]; + final metadataLeafCount = showMeta?.leafCount; + final storedCount = _totalEpisodeCounts[globalKey]; + final downloadedCount = episodes.length; + + appLogger.d( + '📊 Episode count sources for show $showRatingKey:\n' + ' - Metadata leafCount: $metadataLeafCount\n' + ' - Stored count: $storedCount\n' + ' - Downloaded episodes: $downloadedCount\n' + ' - Show metadata exists: ${showMeta != null}\n' + ' - Show type: ${showMeta?.type}\n' + ' - Show title: ${showMeta?.title}', + ); + + // Get total episode count - FIXED: Use metadata.leafCount as primary source + int totalEpisodes; + String countSource; + + if (metadataLeafCount != null && metadataLeafCount > 0) { + totalEpisodes = metadataLeafCount; + countSource = 'metadata.leafCount'; + } else if (storedCount != null && storedCount > 0) { + totalEpisodes = storedCount; + countSource = 'stored count (SharedPreferences)'; + } else { + totalEpisodes = downloadedCount; + countSource = 'downloaded episodes (fallback)'; + } + + appLogger.d( + '✅ Using totalEpisodes=$totalEpisodes from [$countSource] for show $showRatingKey', + ); + + // If we have stored count but no downloads, check if it's a valid partial state + if (totalEpisodes == 0 || (episodes.isEmpty && totalEpisodes > 0)) { + // No episodes downloaded yet, but we have a count stored + // This means user hasn't downloaded anything or deleted all episodes + appLogger.d( + '⚠️ No valid downloads for show $showRatingKey, returning null', + ); + return null; + } + + // Calculate aggregate statistics + int completedCount = 0; + int downloadingCount = 0; + int queuedCount = 0; + int failedCount = 0; + + for (final ep in episodes) { + switch (ep.status) { + case DownloadStatus.completed: + completedCount++; + break; + case DownloadStatus.downloading: + downloadingCount++; + break; + case DownloadStatus.queued: + queuedCount++; + break; + case DownloadStatus.failed: + failedCount++; + break; + default: + break; + } + } + + // Determine overall status + final DownloadStatus overallStatus; + if (completedCount == totalEpisodes) { + // All episodes fully downloaded + overallStatus = DownloadStatus.completed; + } else if (completedCount > 0 && + downloadingCount == 0 && + queuedCount == 0 && + completedCount < totalEpisodes) { + // Some episodes downloaded, but not all, and nothing actively downloading + overallStatus = DownloadStatus.partial; + } else if (downloadingCount > 0) { + overallStatus = DownloadStatus.downloading; + } else if (queuedCount > 0) { + overallStatus = DownloadStatus.queued; + } else if (failedCount > 0) { + overallStatus = DownloadStatus.failed; + } else { + return null; + } + + // Calculate overall progress percentage based on TOTAL episodes + final int overallProgress = totalEpisodes > 0 + ? ((completedCount * 100) / totalEpisodes).round() + : 0; + + appLogger.d( + 'Aggregate progress for show $showRatingKey: $overallProgress% ' + '($completedCount completed, $downloadingCount downloading, ' + '$queuedCount queued of $totalEpisodes total) - Status: $overallStatus', + ); + + return DownloadProgress( + globalKey: globalKey, + status: overallStatus, + progress: overallProgress, + downloadedBytes: 0, // Not tracked at show level + totalBytes: 0, + currentFile: '$completedCount/$totalEpisodes episodes', + ); + } + + /// Calculate aggregate progress for a season (based on all its episodes) + /// Returns synthetic DownloadProgress with aggregated values + DownloadProgress? getAggregateProgressForSeason( + String serverId, + String seasonRatingKey, + ) { + final globalKey = '$serverId:$seasonRatingKey'; + final episodes = _getEpisodeDownloadsForSeason(seasonRatingKey); + + // DIAGNOSTIC: Check all sources of episode count + final seasonMeta = _metadata[globalKey]; + final metadataLeafCount = seasonMeta?.leafCount; + final storedCount = _totalEpisodeCounts[globalKey]; + final downloadedCount = episodes.length; + + appLogger.d( + '📊 Episode count sources for season $seasonRatingKey:\n' + ' - Metadata leafCount: $metadataLeafCount\n' + ' - Stored count: $storedCount\n' + ' - Downloaded episodes: $downloadedCount\n' + ' - Season metadata exists: ${seasonMeta != null}\n' + ' - Season type: ${seasonMeta?.type}\n' + ' - Season title: ${seasonMeta?.title}', + ); + + // Get total episode count - FIXED: Use metadata.leafCount as primary source + int totalEpisodes; + String countSource; + + if (metadataLeafCount != null && metadataLeafCount > 0) { + totalEpisodes = metadataLeafCount; + countSource = 'metadata.leafCount'; + } else if (storedCount != null && storedCount > 0) { + totalEpisodes = storedCount; + countSource = 'stored count (SharedPreferences)'; + } else { + totalEpisodes = downloadedCount; + countSource = 'downloaded episodes (fallback)'; + } + + appLogger.d( + '✅ Using totalEpisodes=$totalEpisodes from [$countSource] for season $seasonRatingKey', + ); + + // If we have stored count but no downloads, check if it's a valid partial state + if (totalEpisodes == 0 || (episodes.isEmpty && totalEpisodes > 0)) { + appLogger.d( + '⚠️ No valid downloads for season $seasonRatingKey, returning null', + ); + return null; + } + + // Calculate aggregate statistics + int completedCount = 0; + int downloadingCount = 0; + int queuedCount = 0; + int failedCount = 0; + + for (final ep in episodes) { + switch (ep.status) { + case DownloadStatus.completed: + completedCount++; + break; + case DownloadStatus.downloading: + downloadingCount++; + break; + case DownloadStatus.queued: + queuedCount++; + break; + case DownloadStatus.failed: + failedCount++; + break; + default: + break; + } + } + + // Determine overall status + final DownloadStatus overallStatus; + if (completedCount == totalEpisodes) { + // All episodes fully downloaded + overallStatus = DownloadStatus.completed; + } else if (completedCount > 0 && + downloadingCount == 0 && + queuedCount == 0 && + completedCount < totalEpisodes) { + // Some episodes downloaded, but not all, and nothing actively downloading + overallStatus = DownloadStatus.partial; + } else if (downloadingCount > 0) { + overallStatus = DownloadStatus.downloading; + } else if (queuedCount > 0) { + overallStatus = DownloadStatus.queued; + } else if (failedCount > 0) { + overallStatus = DownloadStatus.failed; + } else { + return null; + } + + // Calculate overall progress percentage based on TOTAL episodes + final int overallProgress = totalEpisodes > 0 + ? ((completedCount * 100) / totalEpisodes).round() + : 0; + + appLogger.d( + 'Aggregate progress for season $seasonRatingKey: $overallProgress% ' + '($completedCount completed, $downloadingCount downloading, ' + '$queuedCount queued of $totalEpisodes total) - Status: $overallStatus', + ); + + return DownloadProgress( + globalKey: globalKey, + status: overallStatus, + progress: overallProgress, + downloadedBytes: 0, + totalBytes: 0, + currentFile: '$completedCount/$totalEpisodes episodes', + ); + } + + /// Whether there are any downloads (active or completed) + bool get hasDownloads => _downloads.isNotEmpty; + + /// Whether there are any active downloads + bool get hasActiveDownloads => _downloads.values.any( + (p) => + p.status == DownloadStatus.downloading || + p.status == DownloadStatus.queued, + ); + + /// Get download progress for a specific item + /// For shows/seasons, returns aggregate progress of all child episodes + /// For episodes/movies, returns direct progress + DownloadProgress? getProgress(String globalKey) { + // First check if we have direct progress (for episodes/movies) + final directProgress = _downloads[globalKey]; + if (directProgress != null) { + return directProgress; + } + + // If no direct progress, check if this is a show or season + // and calculate aggregate progress from episodes + final parts = globalKey.split(':'); + if (parts.length != 2) return null; + + final serverId = parts[0]; + final ratingKey = parts[1]; + + // Try to get metadata to determine type + final meta = _metadata[globalKey]; + if (meta == null) { + // No metadata stored yet, might be a show/season being queued + // Check if any episodes exist for this as a parent + final episodesAsShow = _getEpisodeDownloadsForShow(ratingKey); + if (episodesAsShow.isNotEmpty) { + return getAggregateProgressForShow(serverId, ratingKey); + } + + final episodesAsSeason = _getEpisodeDownloadsForSeason(ratingKey); + if (episodesAsSeason.isNotEmpty) { + return getAggregateProgressForSeason(serverId, ratingKey); + } + + return null; + } + + // We have metadata, check type + final type = meta.type.toLowerCase(); + if (type == 'show') { + return getAggregateProgressForShow(serverId, ratingKey); + } else if (type == 'season') { + return getAggregateProgressForSeason(serverId, ratingKey); + } + + return null; + } + + /// Check if an item is downloaded + /// For shows/seasons, checks if all episodes are downloaded + bool isDownloaded(String globalKey) { + final progress = getProgress(globalKey); + return progress?.status == DownloadStatus.completed; + } + + /// Check if an item is currently downloading + /// For shows/seasons, checks if any episodes are downloading + bool isDownloading(String globalKey) { + final progress = getProgress(globalKey); + return progress?.status == DownloadStatus.downloading; + } + + /// Check if an item is in the queue + /// For shows/seasons, checks if any episodes are queued + bool isQueued(String globalKey) { + final progress = getProgress(globalKey); + return progress?.status == DownloadStatus.queued; + } + + /// Check if an item is currently being queued (building download queue) + bool isQueueing(String globalKey) => _queueing.contains(globalKey); + + /// Get the local video file path for a downloaded item + /// Returns null if not downloaded or file doesn't exist + Future getVideoFilePath(String globalKey) async { + appLogger.d('getVideoFilePath called with globalKey: $globalKey'); + + final downloadedItem = await _downloadManager.getDownloadedMedia(globalKey); + if (downloadedItem == null) { + appLogger.w('No downloaded item found for globalKey: $globalKey'); + return null; + } + if (downloadedItem.status != DownloadStatus.completed.index) { + appLogger.w('Download not complete. Status: ${downloadedItem.status}'); + return null; + } + if (downloadedItem.videoFilePath == null) { + appLogger.w('Video file path is null for globalKey: $globalKey'); + return null; + } + + final storedPath = downloadedItem.videoFilePath!; + final storageService = DownloadStorageService.instance; + + // SAF URIs (content://) are already valid - don't transform them + if (storageService.isSafUri(storedPath)) { + appLogger.d('Found SAF video path: $storedPath'); + return storedPath; + } + + // Convert stored path (may be relative) to absolute path + final absolutePath = await storageService.ensureAbsolutePath(storedPath); + + // Verify file exists + final file = File(absolutePath); + if (!await file.exists()) { + appLogger.w('Offline video file not found: $absolutePath'); + return null; + } + return absolutePath; + } + + /// Queue a download for a media item. + /// For movies and episodes, queues directly. + /// For shows and seasons, fetches all child episodes and queues them. + /// Returns the number of items queued. + Future queueDownload(PlexMetadata metadata, PlexClient client) async { + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + + // Check if downloads are blocked on cellular + if (await DownloadManagerService.shouldBlockDownloadOnCellular()) { + throw CellularDownloadBlockedException(); + } + + try { + // Mark as queueing to show loading state in UI + _queueing.add(globalKey); + notifyListeners(); + + final type = metadata.type.toLowerCase(); + + if (type == 'movie' || type == 'episode') { + // Direct download of a single item + await _queueSingleDownload(metadata, client); + return 1; + } else if (type == 'show') { + // Store show metadata so getProgress() can identify it as a show + _metadata[globalKey] = metadata; + + // Download all episodes from all seasons + return await _queueShowDownload(metadata, client); + } else if (type == 'season') { + // Store season metadata so getProgress() can identify it as a season + _metadata[globalKey] = metadata; + + // Download all episodes in season + return await _queueSeasonDownload(metadata, client); + } else { + throw Exception('Cannot download ${metadata.type}'); + } + } finally { + // Always remove from queueing set, even on error + _queueing.remove(globalKey); + notifyListeners(); + } + } + + /// Queue a single movie or episode for download + Future _queueSingleDownload( + PlexMetadata metadata, + PlexClient client, + ) async { + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + + // Don't re-queue if already downloading or completed + if (_downloads.containsKey(globalKey)) { + final existing = _downloads[globalKey]!; + if (existing.status == DownloadStatus.downloading || + existing.status == DownloadStatus.completed) { + return; + } + } + + // Fetch full metadata to get year, summary, clearLogo + // The metadata from getChildren() is summarized and missing these fields + PlexMetadata metadataToStore = metadata; + try { + final fullMetadata = await client.getMetadataWithImages( + metadata.ratingKey, + ); + if (fullMetadata != null) { + metadataToStore = fullMetadata.copyWith( + serverId: metadata.serverId, + serverName: metadata.serverName, + ); + } + } catch (e) { + appLogger.w( + 'Failed to fetch full metadata for ${metadata.ratingKey}, using partial', + error: e, + ); + } + + // For episodes, also fetch and store show and season metadata for offline display + if (metadataToStore.type == 'episode') { + await _fetchAndStoreParentMetadata(metadataToStore, client); + } + + // Store full metadata for display + _metadata[globalKey] = metadataToStore; + + // Update local state immediately for UI feedback + _downloads[globalKey] = DownloadProgress( + globalKey: globalKey, + status: DownloadStatus.queued, + ); + notifyListeners(); + + // Actually trigger download via DownloadManagerService + await _downloadManager.queueDownload( + metadata: metadataToStore, + client: client, + ); + } + + /// Fetch and store show and season metadata for an episode + /// Also downloads artwork for show and season + Future _fetchAndStoreParentMetadata( + PlexMetadata episode, + PlexClient client, + ) async { + final serverId = episode.serverId; + if (serverId == null) return; + final storageService = DownloadStorageService.instance; + + // Fetch and store show metadata if not already stored + final showRatingKey = episode.grandparentRatingKey; + if (showRatingKey != null) { + final showGlobalKey = '$serverId:$showRatingKey'; + + // Try to use existing metadata (set when queueing an entire show) + PlexMetadata? showMetadata = _metadata[showGlobalKey]; + + // If not already cached, fetch full metadata with images + if (showMetadata == null) { + try { + showMetadata = await client.getMetadataWithImages(showRatingKey); + } catch (e) { + appLogger.w( + 'Failed to fetch show metadata for $showRatingKey', + error: e, + ); + } + } + + if (showMetadata != null) { + final showWithServer = showMetadata.copyWith(serverId: serverId); + _metadata[showGlobalKey] = showWithServer; + + // Persist to database/API cache for offline usage + await _downloadManager.saveMetadata(showWithServer); + + // Ensure show artwork is downloaded even if metadata already existed + final thumbPath = showWithServer.thumb; + final hasPoster = thumbPath != null && + await storageService.artworkExists(serverId, thumbPath); + if (!hasPoster) { + await _downloadManager.downloadArtworkForMetadata( + showWithServer, + client, + ); + appLogger.d('Downloaded show artwork for $showGlobalKey'); + } + + // Store artwork reference in provider's map for offline display + _artworkPaths[showGlobalKey] = DownloadedArtwork( + thumbPath: thumbPath, + ); + } + } + + // Fetch and store season metadata if not already stored + final seasonRatingKey = episode.parentRatingKey; + if (seasonRatingKey != null) { + final seasonGlobalKey = '$serverId:$seasonRatingKey'; + PlexMetadata? seasonMetadata = _metadata[seasonGlobalKey]; + + if (seasonMetadata == null) { + try { + seasonMetadata = await client.getMetadataWithImages(seasonRatingKey); + } catch (e) { + appLogger.w( + 'Failed to fetch season metadata for $seasonRatingKey', + error: e, + ); + } + } + + if (seasonMetadata != null) { + final seasonWithServer = seasonMetadata.copyWith(serverId: serverId); + _metadata[seasonGlobalKey] = seasonWithServer; + + // Persist to database/API cache for offline usage + await _downloadManager.saveMetadata(seasonWithServer); + + // Ensure season artwork is downloaded even if metadata already existed + final thumbPath = seasonWithServer.thumb; + final hasPoster = thumbPath != null && + await storageService.artworkExists(serverId, thumbPath); + if (!hasPoster) { + await _downloadManager.downloadArtworkForMetadata( + seasonWithServer, + client, + ); + appLogger.d('Downloaded season artwork for $seasonGlobalKey'); + } + + // Store artwork reference in provider's map for offline display + _artworkPaths[seasonGlobalKey] = DownloadedArtwork( + thumbPath: thumbPath, + ); + } + } + } + + /// Queue all episodes from a TV show for download + Future _queueShowDownload(PlexMetadata show, PlexClient client) async { + final globalKey = '${show.serverId}:${show.ratingKey}'; + int count = 0; + final seasons = await client.getChildren(show.ratingKey); + + // Store total episode count from show metadata (leafCount) + if (show.leafCount != null && show.leafCount! > 0) { + _totalEpisodeCounts[globalKey] = show.leafCount!; + await _persistTotalEpisodeCount(globalKey, show.leafCount!); + appLogger.i( + '💾 Stored episode count for show $globalKey: ${show.leafCount}\n' + ' - Show title: ${show.title}\n' + ' - Show type: ${show.type}\n' + ' - Total stored counts: ${_totalEpisodeCounts.length}', + ); + } else { + appLogger.w( + '⚠️ Show $globalKey has no leafCount! Cannot store episode count.\n' + ' - Show title: ${show.title}\n' + ' - Show type: ${show.type}\n' + ' - leafCount value: ${show.leafCount}', + ); + } + + for (final season in seasons) { + if (season.type == 'season') { + // Ensure season has serverId from parent show + final seasonWithServer = season.serverId != null + ? season + : season.copyWith(serverId: show.serverId); + count += await _queueSeasonDownload(seasonWithServer, client); + } + } + + return count; + } + + /// Queue all episodes from a season for download + Future _queueSeasonDownload( + PlexMetadata season, + PlexClient client, + ) async { + final globalKey = '${season.serverId}:${season.ratingKey}'; + int count = 0; + final episodes = await client.getChildren(season.ratingKey); + + // Store total episode count from season metadata (leafCount) + if (season.leafCount != null && season.leafCount! > 0) { + _totalEpisodeCounts[globalKey] = season.leafCount!; + await _persistTotalEpisodeCount(globalKey, season.leafCount!); + appLogger.i( + '💾 Stored episode count for season $globalKey: ${season.leafCount}\n' + ' - Season title: ${season.title}\n' + ' - Season type: ${season.type}\n' + ' - Total stored counts: ${_totalEpisodeCounts.length}', + ); + } else { + appLogger.w( + '⚠️ Season $globalKey has no leafCount! Cannot store episode count.\n' + ' - Season title: ${season.title}\n' + ' - Season type: ${season.type}\n' + ' - leafCount value: ${season.leafCount}', + ); + } + + for (final episode in episodes) { + if (episode.type == 'episode') { + // Ensure episode has serverId from parent season + final episodeWithServer = episode.serverId != null + ? episode + : episode.copyWith(serverId: season.serverId); + await _queueSingleDownload(episodeWithServer, client); + count++; + } + } + + return count; + } + + /// Queue only the missing (not downloaded) episodes for a show/season + /// Used for resuming partial downloads + /// Returns the number of episodes queued + Future queueMissingEpisodes( + PlexMetadata metadata, + PlexClient client, + ) async { + final type = metadata.type.toLowerCase(); + + if (type == 'show') { + return await _queueMissingShowEpisodes(metadata, client); + } else if (type == 'season') { + return await _queueMissingSeasonEpisodes(metadata, client); + } else { + throw Exception('queueMissingEpisodes only supports shows/seasons'); + } + } + + /// Queue missing episodes for a show + Future _queueMissingShowEpisodes( + PlexMetadata show, + PlexClient client, + ) async { + int queuedCount = 0; + + // Fetch all seasons + final seasons = await client.getChildren(show.ratingKey); + + for (final season in seasons) { + if (season.type == 'season') { + final seasonWithServer = season.serverId != null + ? season + : season.copyWith(serverId: show.serverId); + queuedCount += await _queueMissingSeasonEpisodes( + seasonWithServer, + client, + ); + } + } + + appLogger.i('Queued $queuedCount missing episodes for show ${show.title}'); + return queuedCount; + } + + /// Queue missing episodes for a season + Future _queueMissingSeasonEpisodes( + PlexMetadata season, + PlexClient client, + ) async { + int queuedCount = 0; + + // Fetch all episodes + final episodes = await client.getChildren(season.ratingKey); + + for (final episode in episodes) { + if (episode.type == 'episode') { + final episodeWithServer = episode.serverId != null + ? episode + : episode.copyWith(serverId: season.serverId); + + final episodeGlobalKey = + '${episodeWithServer.serverId}:${episodeWithServer.ratingKey}'; + + // Only queue if NOT already downloaded or in progress + final progress = _downloads[episodeGlobalKey]; + if (progress == null || + (progress.status != DownloadStatus.completed && + progress.status != DownloadStatus.downloading && + progress.status != DownloadStatus.queued)) { + await _queueSingleDownload(episodeWithServer, client); + queuedCount++; + appLogger.d( + 'Queued missing episode: ${episode.title} ($episodeGlobalKey)', + ); + } + } + } + + return queuedCount; + } + + /// Pause a download (works for both downloading and queued items) + Future pauseDownload(String globalKey) async { + final progress = _downloads[globalKey]; + if (progress != null && + (progress.status == DownloadStatus.downloading || + progress.status == DownloadStatus.queued)) { + await _downloadManager.pauseDownload(globalKey); + } + } + + /// Resume a paused download + Future resumeDownload(String globalKey, PlexClient client) async { + final progress = _downloads[globalKey]; + if (progress != null && progress.status == DownloadStatus.paused) { + await _downloadManager.resumeDownload(globalKey, client); + } + } + + /// Retry a failed download + Future retryDownload(String globalKey, PlexClient client) async { + final progress = _downloads[globalKey]; + if (progress != null && progress.status == DownloadStatus.failed) { + await _downloadManager.retryDownload(globalKey, client); + } + } + + /// Cancel a download + Future cancelDownload(String globalKey) async { + final progress = _downloads[globalKey]; + if (progress != null) { + await _downloadManager.cancelDownload(globalKey); + _downloads.remove(globalKey); + _metadata.remove(globalKey); + notifyListeners(); + } + } + + /// Delete a downloaded item + Future deleteDownload(String globalKey) async { + try { + // Check if this is a show/season and clean up episode count + final meta = _metadata[globalKey]; + if (meta?.type == 'show' || meta?.type == 'season') { + final removedCount = _totalEpisodeCounts.remove(globalKey); + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('episode_count_$globalKey'); + appLogger.i( + '🗑️ Removed episode count for $globalKey\n' + ' - Removed count value: $removedCount\n' + ' - Metadata type: ${meta?.type}\n' + ' - Metadata title: ${meta?.title}\n' + ' - Remaining stored counts: ${_totalEpisodeCounts.length}', + ); + } + + // Start deletion (progress will be tracked via stream) + await _downloadManager.deleteDownload(globalKey); + + // Remove from local state + _downloads.remove(globalKey); + _metadata.remove(globalKey); + _artworkPaths.remove(globalKey); + + notifyListeners(); + } catch (e) { + // Remove from deletion tracking on error + _deletionProgress.remove(globalKey); + notifyListeners(); + rethrow; + } + } + + /// Handle deletion progress updates + void _onDeletionProgressUpdate(DeletionProgress progress) { + if (progress.isComplete) { + // Deletion complete - remove from tracking + _deletionProgress.remove(progress.globalKey); + } else { + // Update progress + _deletionProgress[progress.globalKey] = progress; + } + notifyListeners(); + } + + /// Check if an item is being deleted + bool isDeleting(String globalKey) => _deletionProgress.containsKey(globalKey); + + /// Get deletion progress for an item + DeletionProgress? getDeletionProgress(String globalKey) => + _deletionProgress[globalKey]; + + /// Get all items currently being deleted + UnmodifiableMapView get deletionProgress => + UnmodifiableMapView(_deletionProgress); + + /// Refresh the downloads list from database + Future refresh() async { + await _loadPersistedDownloads(); + } + + /// Refresh only metadata from API cache (after watch state sync). + /// + /// This is more lightweight than full refresh() - only updates metadata + /// without reloading download progress from database. + Future refreshMetadataFromCache() async { + final apiCache = PlexApiCache.instance; + int updatedCount = 0; + + for (final globalKey in _metadata.keys.toList()) { + final parts = globalKey.split(':'); + if (parts.length != 2) continue; + + final serverId = parts[0]; + final ratingKey = parts[1]; + + try { + final cached = await apiCache.get( + serverId, + '/library/metadata/$ratingKey', + ); + + if (cached != null) { + final metadataList = cached['MediaContainer']?['Metadata'] as List?; + if (metadataList != null && metadataList.isNotEmpty) { + final metadata = PlexMetadata.fromJson(metadataList.first); + _metadata[globalKey] = metadata.copyWith(serverId: serverId); + updatedCount++; + } + } + } catch (e) { + appLogger.d('Failed to refresh metadata for $globalKey: $e'); + } + } + + if (updatedCount > 0) { + appLogger.i('Refreshed metadata from cache for $updatedCount items'); + notifyListeners(); + } + } +} + +/// Exception thrown when download is blocked due to cellular-only setting +class CellularDownloadBlockedException implements Exception { + final String message = 'Downloads are disabled on cellular data'; + + @override + String toString() => message; +} diff --git a/lib/providers/offline_mode_provider.dart b/lib/providers/offline_mode_provider.dart new file mode 100644 index 00000000..b1c120f3 --- /dev/null +++ b/lib/providers/offline_mode_provider.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import '../services/multi_server_manager.dart'; + +/// Tracks offline mode status based on network connectivity and server reachability. +class OfflineModeProvider extends ChangeNotifier { + final MultiServerManager _serverManager; + + StreamSubscription>? _connectivitySubscription; + StreamSubscription>? _serverStatusSubscription; + + bool _hasNetworkConnection = true; + bool _hasServerConnection = false; + bool _isInitialized = false; + + OfflineModeProvider(this._serverManager); + + /// Whether the app is currently in offline mode + /// Offline = no network OR no servers reachable + bool get isOffline => !_hasNetworkConnection || !_hasServerConnection; + + /// Whether there is network connectivity (WiFi, mobile data, etc.) + bool get hasNetworkConnection => _hasNetworkConnection; + + /// Whether at least one Plex server is reachable + bool get hasServerConnection => _hasServerConnection; + + /// Initialize the provider and start monitoring + Future initialize() async { + if (_isInitialized) return; + _isInitialized = true; + + // Check initial connectivity + final connectivityResult = await Connectivity().checkConnectivity(); + _hasNetworkConnection = !connectivityResult.contains( + ConnectivityResult.none, + ); + + // Monitor connectivity changes + _connectivitySubscription = Connectivity().onConnectivityChanged.listen(( + results, + ) { + final wasOffline = isOffline; + _hasNetworkConnection = !results.contains(ConnectivityResult.none); + + if (wasOffline != isOffline) { + notifyListeners(); + } + }); + + // Monitor server status from MultiServerManager + _serverStatusSubscription = _serverManager.statusStream.listen((statusMap) { + final wasOffline = isOffline; + _hasServerConnection = statusMap.values.any((isOnline) => isOnline); + + if (wasOffline != isOffline) { + notifyListeners(); + } + }); + + // Check initial server status + _hasServerConnection = _serverManager.onlineServerIds.isNotEmpty; + notifyListeners(); + } + + /// Force a refresh of connectivity status + Future refresh() async { + final connectivityResult = await Connectivity().checkConnectivity(); + _hasNetworkConnection = !connectivityResult.contains( + ConnectivityResult.none, + ); + _hasServerConnection = _serverManager.onlineServerIds.isNotEmpty; + notifyListeners(); + } + + @override + void dispose() { + _connectivitySubscription?.cancel(); + _serverStatusSubscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/offline_watch_provider.dart b/lib/providers/offline_watch_provider.dart new file mode 100644 index 00000000..ab592d29 --- /dev/null +++ b/lib/providers/offline_watch_provider.dart @@ -0,0 +1,214 @@ +import 'package:flutter/foundation.dart'; + +import '../models/plex_metadata.dart'; +import '../services/offline_watch_sync_service.dart'; +import '../services/plex_api_cache.dart'; +import 'download_provider.dart'; + +/// Provider for offline watch status UI state. +/// +/// Provides: +/// - Effective watch status (local changes + cached server data) +/// - Offline "OnDeck" calculation for shows +/// - Manual mark watched/unwatched while offline +class OfflineWatchProvider extends ChangeNotifier { + final OfflineWatchSyncService _syncService; + final DownloadProvider _downloadProvider; + // ignore: unused_field - reserved for future cached metadata lookup + final PlexApiCache _apiCache; + + OfflineWatchProvider({ + required OfflineWatchSyncService syncService, + required DownloadProvider downloadProvider, + required PlexApiCache apiCache, + }) : _syncService = syncService, + _downloadProvider = downloadProvider, + _apiCache = apiCache { + // Listen to sync service changes to update UI + _syncService.addListener(_onSyncServiceChanged); + } + + void _onSyncServiceChanged() { + notifyListeners(); + } + + /// Whether a sync is in progress + bool get isSyncing => _syncService.isSyncing; + + /// Get count of pending sync items + Future getPendingSyncCount() => _syncService.getPendingSyncCount(); + + /// Get the effective watch status for a media item. + /// + /// Priority: + /// 1. Local offline action (if exists) + /// 2. Cached server data from API cache + /// 3. Metadata from download provider + /// + /// Returns true if watched, false otherwise. + Future isWatched(String globalKey) async { + // First check local offline action + final localStatus = await _syncService.getLocalWatchStatus(globalKey); + if (localStatus != null) { + return localStatus; + } + + // Fall back to cached metadata + final metadata = _downloadProvider.getMetadata(globalKey); + if (metadata != null) { + return metadata.isWatched; + } + + return false; + } + + /// Check watch status synchronously using cached metadata. + /// + /// This is useful for UI that can't await, but may not reflect + /// the most recent local actions. + bool isWatchedSync(PlexMetadata metadata) { + // Note: This doesn't check local actions synchronously + // because that would require async database access. + // For real-time accuracy, use isWatched() instead. + return metadata.isWatched; + } + + /// Get the effective view offset (resume position) for a media item. + /// + /// Priority: + /// 1. Local offline progress (if exists) + /// 2. Metadata from download provider + /// + /// Returns null if no position is available. + Future getViewOffset(String globalKey) async { + // First check local offline progress + final localOffset = await _syncService.getLocalViewOffset(globalKey); + if (localOffset != null) { + return localOffset; + } + + // Fall back to cached metadata + final metadata = _downloadProvider.getMetadata(globalKey); + return metadata?.viewOffset; + } + + /// Find the next unwatched downloaded episode for a show. + /// + /// This is the "offline OnDeck" calculation - finds the first + /// episode that hasn't been watched (or is in progress). + /// + /// Episodes are sorted by season number, then episode number. + /// + /// Returns the next unwatched episode, or the first episode if all watched. + Future getNextUnwatchedEpisode(String showRatingKey) async { + final episodes = _downloadProvider.getDownloadedEpisodesForShow( + showRatingKey, + ); + + if (episodes.isEmpty) return null; + + // Sort by season, then episode number + episodes.sort((a, b) { + final seasonCompare = (a.parentIndex ?? 0).compareTo(b.parentIndex ?? 0); + if (seasonCompare != 0) return seasonCompare; + return (a.index ?? 0).compareTo(b.index ?? 0); + }); + + // Find first unwatched episode + for (final episode in episodes) { + final watched = await isWatched(episode.globalKey); + if (!watched) { + return episode; + } + } + + // All episodes watched - return first episode for replay + return episodes.first; + } + + /// Find the next unwatched downloaded episode synchronously. + /// + /// This uses cached metadata without checking local offline actions. + /// For real-time accuracy, use getNextUnwatchedEpisode() instead. + PlexMetadata? getNextUnwatchedEpisodeSync(String showRatingKey) { + final episodes = _downloadProvider.getDownloadedEpisodesForShow( + showRatingKey, + ); + + if (episodes.isEmpty) return null; + + // Sort by season, then episode number + episodes.sort((a, b) { + final seasonCompare = (a.parentIndex ?? 0).compareTo(b.parentIndex ?? 0); + if (seasonCompare != 0) return seasonCompare; + return (a.index ?? 0).compareTo(b.index ?? 0); + }); + + // Find first unwatched episode (using metadata's isWatched) + for (final episode in episodes) { + if (!episode.isWatched) { + return episode; + } + } + + // All episodes watched - return first episode for replay + return episodes.first; + } + + /// Mark an item as watched while offline. + /// + /// This queues the action for sync when online. + Future markAsWatched({ + required String serverId, + required String ratingKey, + }) async { + await _syncService.queueMarkWatched( + serverId: serverId, + ratingKey: ratingKey, + ); + notifyListeners(); + } + + /// Mark an item as unwatched while offline. + /// + /// This queues the action for sync when online. + Future markAsUnwatched({ + required String serverId, + required String ratingKey, + }) async { + await _syncService.queueMarkUnwatched( + serverId: serverId, + ratingKey: ratingKey, + ); + notifyListeners(); + } + + /// Get downloaded episodes for a show with their watch status. + /// + /// Returns a list of (episode, isWatched) pairs. + Future> + getEpisodesWithWatchStatus(String showRatingKey) async { + final episodes = _downloadProvider.getDownloadedEpisodesForShow( + showRatingKey, + ); + + final results = <(PlexMetadata, bool)>[]; + for (final episode in episodes) { + final watched = await isWatched(episode.globalKey); + results.add((episode, watched)); + } + + return results; + } + + /// Trigger a manual sync of pending items. + Future syncNow() async { + await _syncService.syncPendingItems(); + } + + @override + void dispose() { + _syncService.removeListener(_onSyncServiceChanged); + super.dispose(); + } +} diff --git a/lib/screens/downloads/downloads_screen.dart b/lib/screens/downloads/downloads_screen.dart new file mode 100644 index 00000000..ce52c032 --- /dev/null +++ b/lib/screens/downloads/downloads_screen.dart @@ -0,0 +1,365 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../models/plex_metadata.dart'; +import '../../providers/download_provider.dart'; +import '../../providers/multi_server_provider.dart'; +import '../../providers/settings_provider.dart'; +import '../../services/gamepad_service.dart'; +import '../../utils/platform_detector.dart'; +import '../../widgets/desktop_app_bar.dart'; +import '../../widgets/focusable_tab_chip.dart'; +import '../../widgets/focusable_media_card.dart'; +import '../../widgets/media_grid_delegate.dart'; +import '../../widgets/download_tree_view.dart'; +import '../main_screen.dart'; +import '../../i18n/strings.g.dart'; + +class DownloadsScreen extends StatefulWidget { + const DownloadsScreen({super.key}); + + @override + State createState() => DownloadsScreenState(); +} + +class DownloadsScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + // Focus nodes for tab chips + final _queueTabChipFocusNode = FocusNode(debugLabel: 'tab_chip_queue'); + final _tvShowsTabChipFocusNode = FocusNode(debugLabel: 'tab_chip_tv_shows'); + final _moviesTabChipFocusNode = FocusNode(debugLabel: 'tab_chip_movies'); + + /// When true, suppress auto-focus in tabs (used when navigating via tab bar) + bool _suppressAutoFocus = false; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(_onTabChanged); + + // Register L1/R1 callbacks for tab navigation + GamepadService.onL1Pressed = _goToPreviousTab; + GamepadService.onR1Pressed = _goToNextTab; + } + + @override + void dispose() { + _tabController.removeListener(_onTabChanged); + _tabController.dispose(); + _queueTabChipFocusNode.dispose(); + _tvShowsTabChipFocusNode.dispose(); + _moviesTabChipFocusNode.dispose(); + // Clear L1/R1 callbacks + GamepadService.onL1Pressed = null; + GamepadService.onR1Pressed = null; + super.dispose(); + } + + void _goToPreviousTab() { + if (_tabController.index > 0) { + setState(() { + _suppressAutoFocus = true; + _tabController.index = _tabController.index - 1; + }); + _getTabChipFocusNode(_tabController.index).requestFocus(); + } + } + + void _goToNextTab() { + if (_tabController.index < _tabController.length - 1) { + setState(() { + _suppressAutoFocus = true; + _tabController.index = _tabController.index + 1; + }); + _getTabChipFocusNode(_tabController.index).requestFocus(); + } + } + + void _onTabChanged() { + if (!_tabController.indexIsChanging) { + // Rebuild to update chip selection state + setState(() {}); + } + } + + /// Get the focus node for a tab chip by index + FocusNode _getTabChipFocusNode(int index) { + switch (index) { + case 0: + return _queueTabChipFocusNode; + case 1: + return _tvShowsTabChipFocusNode; + case 2: + return _moviesTabChipFocusNode; + default: + return _queueTabChipFocusNode; + } + } + + /// Focus the currently selected tab chip in the tab bar. + /// Called when BACK is pressed in tab content. + void focusTabBar() { + setState(() { + _suppressAutoFocus = true; + }); + final focusNode = _getTabChipFocusNode(_tabController.index); + focusNode.requestFocus(); + } + + /// Handle BACK from tab bar - navigate to sidenav + void _onTabBarBack() { + final focusScope = MainScreenFocusScope.of(context); + focusScope?.focusSidebar(); + } + + /// Focus the first item in the currently active tab + void _focusCurrentTab() { + // Re-enable auto-focus since user is navigating into tab content + setState(() { + _suppressAutoFocus = false; + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + // Focus will be handled by the tab content + }); + } + + Widget _buildTabChip(String label, int index) { + final isSelected = _tabController.index == index; + const tabCount = 3; // Queue, TV Shows, Movies + + return FocusableTabChip( + label: label, + isSelected: isSelected, + focusNode: _getTabChipFocusNode(index), + onSelect: () { + if (isSelected) { + // Already selected - navigate to tab content + _focusCurrentTab(); + } else { + // Switch to this tab + setState(() { + _tabController.index = index; + }); + } + }, + onNavigateLeft: index > 0 + ? () { + final newIndex = index - 1; + setState(() { + _suppressAutoFocus = true; + _tabController.index = newIndex; + }); + _getTabChipFocusNode(newIndex).requestFocus(); + } + : null, + onNavigateRight: index < tabCount - 1 + ? () { + final newIndex = index + 1; + setState(() { + _suppressAutoFocus = true; + _tabController.index = newIndex; + }); + _getTabChipFocusNode(newIndex).requestFocus(); + } + : null, + onNavigateDown: _focusCurrentTab, + onBack: _onTabBarBack, + ); + } + + /// Build the app bar title - either tabs on desktop or simple title on mobile + Widget _buildAppBarTitle() { + // On desktop/TV with side nav, show tabs in app bar + if (PlatformDetector.shouldUseSideNavigation(context)) { + return Row( + children: [ + _buildTabChip(t.downloads.manage, 0), + const SizedBox(width: 8), + _buildTabChip(t.downloads.tvShows, 1), + const SizedBox(width: 8), + _buildTabChip(t.downloads.movies, 2), + ], + ); + } + + // On mobile, show simple title + return Text(t.downloads.title); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + DesktopSliverAppBar( + title: _buildAppBarTitle(), + floating: true, + pinned: true, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + scrolledUnderElevation: 0, + ), + SliverFillRemaining( + child: Column( + children: [ + // Tab selector chips (only on mobile - desktop has them in app bar) + if (!PlatformDetector.shouldUseSideNavigation(context)) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildTabChip(t.downloads.manage, 0), + const SizedBox(width: 8), + _buildTabChip(t.downloads.tvShows, 1), + const SizedBox(width: 8), + _buildTabChip(t.downloads.movies, 2), + ], + ), + ), + ), + // Tab content + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + Consumer2( + builder: (context, downloadProvider, serverProvider, _) { + // Helper to get client from globalKey (serverId:ratingKey) + getClient(String globalKey) { + final serverId = globalKey.split(':').first; + return serverProvider.serverManager.getClient(serverId); + } + + return DownloadTreeView( + downloads: downloadProvider.downloads, + metadata: downloadProvider.metadata, + onPause: downloadProvider.pauseDownload, + onResume: (globalKey) { + final client = getClient(globalKey); + if (client != null) { + downloadProvider.resumeDownload(globalKey, client); + } + }, + onRetry: (globalKey) { + final client = getClient(globalKey); + if (client != null) { + downloadProvider.retryDownload(globalKey, client); + } + }, + onCancel: downloadProvider.cancelDownload, + onDelete: downloadProvider.deleteDownload, + ); + }, + ), + _DownloadsGridContent( + type: DownloadType.tvShows, + suppressAutoFocus: _suppressAutoFocus, + onBack: focusTabBar, + ), + _DownloadsGridContent( + type: DownloadType.movies, + suppressAutoFocus: _suppressAutoFocus, + onBack: focusTabBar, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +enum DownloadType { manage, tvShows, movies } + +/// Grid content for TV Shows and Movies tabs +class _DownloadsGridContent extends StatelessWidget { + final DownloadType type; + final bool suppressAutoFocus; + final VoidCallback? onBack; + + const _DownloadsGridContent({ + required this.type, + required this.suppressAutoFocus, + this.onBack, + }); + + @override + Widget build(BuildContext context) { + return Consumer2( + builder: (context, downloadProvider, settingsProvider, _) { + final List items = type == DownloadType.tvShows + ? downloadProvider.downloadedShows + : downloadProvider.downloadedMovies; + + if (items.isEmpty) { + return _buildEmptyState(context); + } + + return GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settingsProvider.libraryDensity, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return FocusableMediaCard( + item: item, + onBack: onBack, + isOffline: true, // Downloaded content works without server + ); + }, + ); + }, + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.download_outlined, + size: 80, + color: Theme.of(context).colorScheme.outline, + ), + const SizedBox(height: 24), + Text( + t.downloads.noDownloads, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + t.downloads.noDownloadsDescription, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index bc450ff2..ca364e2b 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -12,6 +12,8 @@ import '../providers/multi_server_provider.dart'; import '../providers/server_state_provider.dart'; import '../providers/hidden_libraries_provider.dart'; import '../providers/playback_state_provider.dart'; +import '../services/offline_watch_sync_service.dart'; +import '../providers/offline_mode_provider.dart'; import '../services/plex_auth_service.dart'; import '../services/storage_service.dart'; import '../utils/desktop_window_padding.dart'; @@ -19,6 +21,7 @@ import '../widgets/side_navigation_rail.dart'; import 'discover_screen.dart'; import 'libraries/libraries_screen.dart'; import 'search_screen.dart'; +import 'downloads/downloads_screen.dart'; import 'settings/settings_screen.dart'; /// Provides access to the main screen's focus control. @@ -46,22 +49,29 @@ class MainScreenFocusScope extends InheritedWidget { } class MainScreen extends StatefulWidget { - final PlexClient client; + final PlexClient? client; + final bool isOfflineMode; - const MainScreen({super.key, required this.client}); + const MainScreen({super.key, this.client, this.isOfflineMode = false}); @override State createState() => _MainScreenState(); } class _MainScreenState extends State with RouteAware { - int _currentIndex = 0; + late int _currentIndex; String? _selectedLibraryGlobalKey; - late final List _screens; + /// Whether the app is in offline mode (no server connection) + bool _isOffline = false; + + OfflineModeProvider? _offlineModeProvider; + + late List _screens; final GlobalKey> _discoverKey = GlobalKey(); final GlobalKey> _librariesKey = GlobalKey(); final GlobalKey> _searchKey = GlobalKey(); + final GlobalKey> _downloadsKey = GlobalKey(); final GlobalKey> _settingsKey = GlobalKey(); final GlobalKey _sideNavKey = GlobalKey(); @@ -77,28 +87,25 @@ class _MainScreenState extends State with RouteAware { @override void initState() { super.initState(); + _isOffline = widget.isOfflineMode; - _screens = [ - DiscoverScreen( - key: _discoverKey, - onBecameVisible: _onDiscoverBecameVisible, - ), - LibrariesScreen( - key: _librariesKey, - onLibraryOrderChanged: _onLibraryOrderChanged, - ), - SearchScreen(key: _searchKey), - SettingsScreen(key: _settingsKey), - ]; + // Start on Downloads tab when in offline mode + // In offline mode: visual index 0 = Downloads (screen 3), 1 = Settings (screen 4) + // In online mode: indices match directly + _currentIndex = _isOffline ? 0 : 0; - // Set up data invalidation callback for profile switching + _screens = _buildScreens(_isOffline); + + // Set up data invalidation callback for profile switching (skip in offline mode) WidgetsBinding.instance.addPostFrameCallback((_) async { - // Initialize UserProfileProvider to ensure it's ready after sign-in - final userProfileProvider = context.userProfile; - await userProfileProvider.initialize(); + if (!_isOffline) { + // Initialize UserProfileProvider to ensure it's ready after sign-in + final userProfileProvider = context.userProfile; + await userProfileProvider.initialize(); - // Set up data invalidation callback for profile switching - userProfileProvider.setDataInvalidationCallback(_invalidateAllScreens); + // Set up data invalidation callback for profile switching + userProfileProvider.setDataInvalidationCallback(_invalidateAllScreens); + } // Focus content initially (replaces autofocus which caused focus stealing issues) if (!_isSidebarFocused) { @@ -110,17 +117,99 @@ class _MainScreenState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); + + // Listen for offline/online transitions to refresh navigation & screens + final provider = context.read(); + if (provider != null && provider != _offlineModeProvider) { + _offlineModeProvider?.removeListener(_handleOfflineStatusChanged); + _offlineModeProvider = provider; + _offlineModeProvider!.addListener(_handleOfflineStatusChanged); + _handleOfflineStatusChanged(); + } + routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute); } @override void dispose() { routeObserver.unsubscribe(this); + _offlineModeProvider?.removeListener(_handleOfflineStatusChanged); _sidebarFocusScope.dispose(); _contentFocusScope.dispose(); super.dispose(); } + List _buildScreens(bool offline) { + // In offline mode, only show Downloads and Settings + // In online mode, show all 5 screens + if (offline) { + return [ + DownloadsScreen(key: _downloadsKey), + SettingsScreen(key: _settingsKey), + ]; + } + + return [ + DiscoverScreen( + key: _discoverKey, + onBecameVisible: _onDiscoverBecameVisible, + ), + LibrariesScreen( + key: _librariesKey, + onLibraryOrderChanged: _onLibraryOrderChanged, + ), + SearchScreen(key: _searchKey), + DownloadsScreen(key: _downloadsKey), + SettingsScreen(key: _settingsKey), + ]; + } + + int _normalizeIndexForMode(int current, bool offline) { + if (offline) { + // Only two tabs exist offline: 0 = Downloads, 1 = Settings + if (current <= 0) return 0; + if (current == 1) return 1; + return 0; + } + + // Map offline indices back to online equivalents when reconnecting + if (current == 0) return 3; // Downloads tab + if (current == 1) return 4; // Settings tab + if (current < 0) return 0; + if (current > 4) return 0; + return current; + } + + void _handleOfflineStatusChanged() { + final newOffline = + _offlineModeProvider?.isOffline ?? widget.isOfflineMode; + + if (newOffline == _isOffline) return; + + setState(() { + _isOffline = newOffline; + _screens = _buildScreens(_isOffline); + _selectedLibraryGlobalKey = + _isOffline ? null : _selectedLibraryGlobalKey; + _currentIndex = _normalizeIndexForMode(_currentIndex, _isOffline); + }); + + // Refresh sidebar focus after rebuilding navigation + WidgetsBinding.instance.addPostFrameCallback((_) { + _sideNavKey.currentState?.focusActiveItem(); + }); + + // Ensure profile provider is initialized when coming back online + if (!_isOffline) { + final userProfileProvider = context.userProfile; + userProfileProvider.initialize().then((_) { + userProfileProvider.setDataInvalidationCallback( + _invalidateAllScreens, + ); + }); + } + } + void _focusSidebar() { setState(() => _isSidebarFocused = true); _sidebarFocusScope.requestFocus(); @@ -169,7 +258,7 @@ class _MainScreenState extends State with RouteAware { @override void didPush() { // Called when this route has been pushed (initial navigation) - if (_currentIndex == 0) { + if (_currentIndex == 0 && !_isOffline) { _onDiscoverBecameVisible(); } } @@ -177,7 +266,7 @@ class _MainScreenState extends State with RouteAware { @override void didPopNext() { // Called when returning to this route from a child route (e.g., from video player) - if (_currentIndex == 0) { + if (_currentIndex == 0 && !_isOffline) { _onDiscoverBecameVisible(); } } @@ -221,6 +310,11 @@ class _MainScreenState extends State with RouteAware { appLogger.d( 'Reconnected to $connectedCount/${servers.length} servers after profile switch', ); + + // Trigger watch state sync now that servers are connected + if (connectedCount > 0 && context.mounted) { + context.read().onServersConnected(); + } } // Reset other provider states @@ -254,6 +348,10 @@ class _MainScreenState extends State with RouteAware { setState(() { _currentIndex = index; }); + + // Skip screen-specific logic in offline mode (only Downloads and Settings available) + if (_isOffline) return; + // Notify discover screen when it becomes visible via tab switch if (index == 0) { _onDiscoverBecameVisible(); @@ -315,6 +413,7 @@ class _MainScreenState extends State with RouteAware { key: _sideNavKey, selectedIndex: _currentIndex, selectedLibraryKey: _selectedLibraryGlobalKey, + isOfflineMode: _isOffline, onDestinationSelected: (index) { _selectTab(index); _focusContent(); @@ -344,33 +443,92 @@ class _MainScreenState extends State with RouteAware { ); } + // In offline mode, only show Downloads and Settings + final destinations = _isOffline + ? [ + NavigationDestination( + icon: const Icon(Icons.download_outlined), + selectedIcon: const Icon(Icons.download), + label: t.navigation.downloads, + ), + NavigationDestination( + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + label: t.navigation.settings, + ), + ] + : [ + NavigationDestination( + icon: const Icon(Icons.home_outlined), + selectedIcon: const Icon(Icons.home), + label: t.navigation.home, + ), + NavigationDestination( + icon: const Icon(Icons.video_library_outlined), + selectedIcon: const Icon(Icons.video_library), + label: t.navigation.libraries, + ), + NavigationDestination( + icon: const Icon(Icons.search), + selectedIcon: const Icon(Icons.search), + label: t.navigation.search, + ), + NavigationDestination( + icon: const Icon(Icons.download_outlined), + selectedIcon: const Icon(Icons.download), + label: t.navigation.downloads, + ), + NavigationDestination( + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + label: t.navigation.settings, + ), + ]; + return Scaffold( body: IndexedStack(index: _currentIndex, children: _screens), bottomNavigationBar: NavigationBar( selectedIndex: _currentIndex, onDestinationSelected: _selectTab, - destinations: [ - NavigationDestination( - icon: const Icon(Icons.home_outlined), - selectedIcon: const Icon(Icons.home), - label: t.navigation.home, - ), - NavigationDestination( - icon: const Icon(Icons.video_library_outlined), - selectedIcon: const Icon(Icons.video_library), - label: t.navigation.libraries, - ), - NavigationDestination( - icon: const Icon(Icons.search), - selectedIcon: const Icon(Icons.search), - label: t.navigation.search, - ), - NavigationDestination( - icon: const Icon(Icons.settings_outlined), - selectedIcon: const Icon(Icons.settings), - label: t.navigation.settings, - ), - ], + destinations: destinations, + ), + ); + } +} + +/// Placeholder widget shown for network-dependent screens when offline +class _OfflinePlaceholder extends StatelessWidget { + const _OfflinePlaceholder(); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_off, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + t.messages.youAreOffline, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + t.messages.offlineFeatureUnavailable, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), ), ); } diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index c54abd48..1547a945 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -9,7 +11,10 @@ import '../widgets/plex_optimized_image.dart'; import '../utils/plex_image_helper.dart'; import '../../services/plex_client.dart'; import '../models/plex_metadata.dart'; +import '../models/download_status.dart'; import '../providers/playback_state_provider.dart'; +import '../providers/download_provider.dart'; +import '../providers/offline_watch_provider.dart'; import '../theme/theme_helper.dart'; import '../utils/app_logger.dart'; import '../utils/content_rating_formatter.dart'; @@ -24,8 +29,13 @@ import 'season_detail_screen.dart'; class MediaDetailScreen extends StatefulWidget { final PlexMetadata metadata; + final bool isOffline; - const MediaDetailScreen({super.key, required this.metadata}); + const MediaDetailScreen({ + super.key, + required this.metadata, + this.isOffline = false, + }); @override State createState() => _MediaDetailScreenState(); @@ -53,8 +63,64 @@ class _MediaDetailScreenState extends State { super.dispose(); } + /// Build title text widget for clear logo fallback + Widget _buildTitleText(BuildContext context, String title) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + title, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + shadows: [ + Shadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 8), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + } + + /// Build radial progress indicator for download button + /// If progressPercent is null or 0, shows indeterminate spinner + Widget _buildRadialProgress(double? progressPercent) { + return SizedBox( + width: 20, + height: 20, + child: Stack( + alignment: Alignment.center, + children: [ + // Background circle (only show if we have determinate progress) + if (progressPercent != null && progressPercent > 0) + CircularProgressIndicator( + value: 1.0, + strokeWidth: 2.0, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary.withOpacity(0.2), + ), + ), + // Progress circle (indeterminate if no progress, determinate otherwise) + CircularProgressIndicator( + value: (progressPercent != null && progressPercent > 0) + ? progressPercent + : null, // null = indeterminate + strokeWidth: 2.0, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ); + } + /// Get the correct PlexClient for this metadata's server - PlexClient _getClientForMetadata(BuildContext context) { + /// Returns null in offline mode or if serverId is null + PlexClient? _getClientForMetadata(BuildContext context) { + if (widget.isOffline || widget.metadata.serverId == null) { + return null; + } return context.getClientForServer(widget.metadata.serverId!); } @@ -63,9 +129,34 @@ class _MediaDetailScreenState extends State { _isLoadingMetadata = true; }); + // Offline mode: use passed metadata directly, load seasons from downloads + if (widget.isOffline) { + final type = widget.metadata.type.toLowerCase(); + + setState(() { + _fullMetadata = widget.metadata; + _isLoadingMetadata = false; + }); + + if (type == 'show') { + _loadSeasonsFromDownloads(); + // Get offline OnDeck episode + _loadOfflineOnDeckEpisode(); + } + return; + } + try { // Use server-specific client for this metadata final client = _getClientForMetadata(context); + if (client == null) { + // No client available, use passed metadata + setState(() { + _fullMetadata = widget.metadata; + _isLoadingMetadata = false; + }); + return; + } // Fetch full metadata with clearLogo and OnDeck episode final result = await client.getMetadataWithImagesAndOnDeck( @@ -129,7 +220,8 @@ class _MediaDetailScreenState extends State { // Use server-specific client for this metadata final client = _getClientForMetadata(context); - final seasons = await client.getChildren(widget.metadata.ratingKey); + final seasons = + await client?.getChildren(widget.metadata.ratingKey) ?? []; // Preserve serverId for each season final seasonsWithServerId = seasons .map( @@ -150,12 +242,73 @@ class _MediaDetailScreenState extends State { } } + /// Load seasons from downloaded episodes (offline mode) + void _loadSeasonsFromDownloads() { + setState(() { + _isLoadingSeasons = true; + }); + + final downloadProvider = context.read(); + final episodes = downloadProvider.getDownloadedEpisodesForShow( + widget.metadata.ratingKey, + ); + + // Group episodes by season + final Map> seasonMap = {}; + for (final episode in episodes) { + final seasonNum = episode.parentIndex ?? 0; + seasonMap.putIfAbsent(seasonNum, () => []).add(episode); + } + + // Create season metadata from episodes + final seasons = seasonMap.entries.map((entry) { + final firstEp = entry.value.first; + return PlexMetadata( + ratingKey: firstEp.parentRatingKey ?? '', + key: '/library/metadata/${firstEp.parentRatingKey}', + type: 'season', + title: firstEp.parentTitle ?? 'Season ${entry.key}', + index: entry.key, + thumb: firstEp.parentThumb, + parentRatingKey: firstEp.grandparentRatingKey, + serverId: widget.metadata.serverId, + serverName: widget.metadata.serverName, + ); + }).toList()..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0)); + + setState(() { + _seasons = seasons; + _isLoadingSeasons = false; + }); + } + + /// Load the next unwatched episode for offline mode (offline OnDeck) + Future _loadOfflineOnDeckEpisode() async { + final offlineWatchProvider = context.read(); + final nextEpisode = await offlineWatchProvider.getNextUnwatchedEpisode( + widget.metadata.ratingKey, + ); + + if (nextEpisode != null && mounted) { + setState(() { + _onDeckEpisode = nextEpisode; + }); + appLogger.d( + 'Offline OnDeck: S${nextEpisode.parentIndex}E${nextEpisode.index} - ${nextEpisode.title}', + ); + } + } + /// Update watch state without full screen rebuild /// This preserves scroll position and only updates watch-related data Future _updateWatchState() async { + // Skip in offline mode + if (widget.isOffline) return; + try { // Use server-specific client for this metadata final client = _getClientForMetadata(context); + if (client == null) return; final metadata = await client.getMetadataWithImages( widget.metadata.ratingKey, @@ -199,12 +352,13 @@ class _MediaDetailScreenState extends State { Future _playFirstEpisode() async { try { - // Use server-specific client for this metadata - final client = _getClientForMetadata(context); - // If seasons aren't loaded yet, wait for them or load them if (_seasons.isEmpty && !_isLoadingSeasons) { - await _loadSeasons(); + if (widget.isOffline) { + _loadSeasonsFromDownloads(); + } else { + await _loadSeasons(); + } } // Wait for seasons to finish loading if they're currently loading @@ -225,7 +379,24 @@ class _MediaDetailScreenState extends State { final firstSeason = _seasons.first; // Get episodes of the first season - final episodes = await client.getChildren(firstSeason.ratingKey); + List episodes; + if (widget.isOffline) { + // In offline mode, get episodes from downloads + final downloadProvider = context.read(); + final allEpisodes = downloadProvider.getDownloadedEpisodesForShow( + widget.metadata.ratingKey, + ); + // Filter to episodes of this season + episodes = + allEpisodes + .where((ep) => ep.parentIndex == firstSeason.index) + .toList() + ..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0)); + } else { + final client = _getClientForMetadata(context); + if (client == null) return; + episodes = await client.getChildren(firstSeason.ratingKey); + } if (episodes.isEmpty) { if (mounted) { @@ -244,12 +415,17 @@ class _MediaDetailScreenState extends State { serverName: widget.metadata.serverName, ); if (mounted) { - // Client already retrieved earlier in the method appLogger.d('Playing first episode: ${episodeWithServerId.title}'); - await navigateToVideoPlayer(context, metadata: episodeWithServerId); + await navigateToVideoPlayer( + context, + metadata: episodeWithServerId, + isOffline: widget.isOffline, + ); appLogger.d('Returned from playback, refreshing metadata'); - // Refresh metadata when returning from video player - _loadFullMetadata(); + // Refresh metadata when returning from video player (skip if offline) + if (!widget.isOffline) { + _loadFullMetadata(); + } } } catch (e) { if (mounted) { @@ -261,11 +437,23 @@ class _MediaDetailScreenState extends State { } /// Handle shuffle play using play queues + /// Note: Shuffle requires server connectivity (play queue API) Future _handleShufflePlayWithQueue( BuildContext context, PlexMetadata metadata, ) async { + // Shuffle requires server connectivity + if (widget.isOffline) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Shuffle not available offline')), + ); + } + return; + } + final client = _getClientForMetadata(context); + if (client == null) return; final playbackState = context.read(); final itemType = metadata.type.toLowerCase(); @@ -401,6 +589,37 @@ class _MediaDetailScreenState extends State { if (metadata.art != null) Builder( builder: (context) { + // Check for offline local file first + if (widget.isOffline && + widget.metadata.serverId != null) { + final localPath = context + .read() + .getArtworkLocalPath( + widget.metadata.serverId!, + metadata.art, + ); + if (localPath != null && + File(localPath).existsSync()) { + return Image.file( + File(localPath), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + ), + ); + } + // Offline but no local file - show placeholder + return Container( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + ); + } + + // Online - use network image final client = _getClientForMetadata(context); final mediaQuery = MediaQuery.of(context); final imageUrl = PlexImageHelper.getOptimizedImageUrl( @@ -470,6 +689,37 @@ class _MediaDetailScreenState extends State { width: 400, child: Builder( builder: (context) { + // Check for offline local file first + if (widget.isOffline && + widget.metadata.serverId != null) { + final localPath = context + .read() + .getArtworkLocalPath( + widget.metadata.serverId!, + metadata.clearLogo, + ); + if (localPath != null && + File(localPath).existsSync()) { + return Image.file( + File(localPath), + fit: BoxFit.contain, + alignment: Alignment.centerLeft, + errorBuilder: + (context, error, stackTrace) => + _buildTitleText( + context, + metadata.title, + ), + ); + } + // Offline but no local file - show title text + return _buildTitleText( + context, + metadata.title, + ); + } + + // Online - use network image final client = _getClientForMetadata( context, ); @@ -520,29 +770,9 @@ class _MediaDetailScreenState extends State { ), ), errorWidget: (context, url, error) { - return Align( - alignment: Alignment.centerLeft, - child: Text( - metadata.title, - style: Theme.of(context) - .textTheme - .displaySmall - ?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - shadows: [ - Shadow( - color: Colors.black - .withValues( - alpha: 0.5, - ), - blurRadius: 8, - ), - ], - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + return _buildTitleText( + context, + metadata.title, ); }, ); @@ -748,12 +978,15 @@ class _MediaDetailScreenState extends State { await navigateToVideoPlayer( context, metadata: _onDeckEpisode!, + isOffline: widget.isOffline, ); appLogger.d( 'Returned from playback, refreshing metadata', ); // Refresh metadata when returning from video player - _loadFullMetadata(); + if (!widget.isOffline) { + _loadFullMetadata(); + } } else { // No on deck episode, fetch first episode of first season await _playFirstEpisode(); @@ -764,15 +997,21 @@ class _MediaDetailScreenState extends State { await navigateToVideoPlayer( context, metadata: metadata, + isOffline: widget.isOffline, ); appLogger.d( 'Returned from playback, refreshing metadata', ); // Refresh metadata when returning from video player - _loadFullMetadata(); + if (!widget.isOffline) { + _loadFullMetadata(); + } } }, - icon: const Icon(Icons.play_arrow, size: 20), + icon: Icon( + _getPlayButtonIcon(metadata), + size: 20, + ), label: Text( _getPlayButtonLabel(metadata), style: const TextStyle(fontSize: 16), @@ -806,21 +1045,449 @@ class _MediaDetailScreenState extends State { ), const SizedBox(width: 12), ], + // Download button (hide in offline mode - already downloaded) + if (!widget.isOffline) + Consumer( + builder: (context, downloadProvider, _) { + final globalKey = + '${metadata.serverId}:${metadata.ratingKey}'; + final progress = downloadProvider.getProgress( + globalKey, + ); + final isQueueing = downloadProvider.isQueueing( + globalKey, + ); + + // Debug logging + if (progress != null) { + appLogger.d( + 'UI rebuilding for $globalKey: status=${progress.status}, progress=${progress.progress}%', + ); + } + + // State 1: Queueing (building download queue) + if (isQueueing) { + return IconButton.filledTonal( + onPressed: null, + icon: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + ), + ); + } + + // State 2: Queued (waiting to download) + if (progress?.status == DownloadStatus.queued) { + final currentFile = progress?.currentFile; + final tooltip = + currentFile != null && + currentFile.contains('episodes') + ? 'Queued $currentFile' + : 'Queued'; + + return IconButton.filledTonal( + onPressed: null, + tooltip: tooltip, + icon: const Icon(Icons.schedule), + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + ), + ); + } + + // State 3: Downloading (active download) + if (progress?.status == + DownloadStatus.downloading) { + // Show episode count in tooltip for shows/seasons + final currentFile = progress?.currentFile; + final tooltip = + currentFile != null && + currentFile.contains('episodes') + ? 'Downloading $currentFile' + : 'Downloading...'; + + return IconButton.filledTonal( + onPressed: null, + tooltip: tooltip, + icon: _buildRadialProgress( + progress?.progressPercent, + ), + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + ), + ); + } + + // State 4: Paused (can resume) + if (progress?.status == DownloadStatus.paused) { + return IconButton.filledTonal( + onPressed: () async { + final client = _getClientForMetadata( + context, + ); + if (client == null) return; + await downloadProvider.resumeDownload( + globalKey, + client, + ); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text('Download resumed'), + ), + ); + } + }, + icon: const Icon(Icons.pause_circle_outline), + tooltip: 'Resume download', + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + foregroundColor: Colors.amber, + ), + ); + } + + // State 5: Failed (can retry) + if (progress?.status == DownloadStatus.failed) { + return IconButton.filledTonal( + onPressed: () async { + final client = _getClientForMetadata( + context, + ); + if (client == null) return; + + // Delete failed download and retry + await downloadProvider.deleteDownload( + globalKey, + ); + try { + await downloadProvider.queueDownload( + metadata, + client, + ); + + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t.downloads.downloadQueued, + ), + ), + ); + } + } on CellularDownloadBlockedException { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t + .settings + .cellularDownloadBlocked, + ), + ), + ); + } + } + }, + icon: const Icon(Icons.error_outline), + tooltip: 'Retry download', + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + foregroundColor: Colors.red, + ), + ); + } + + // State 6: Cancelled (can delete or retry) + if (progress?.status == + DownloadStatus.cancelled) { + return IconButton.filledTonal( + onPressed: () async { + // Show options: Delete or Retry + final action = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Cancelled Download'), + content: const Text( + 'This download was cancelled. What would you like to do?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop( + context, + 'delete', + ), + child: Text(t.common.delete), + ), + TextButton( + onPressed: () => + Navigator.pop(context, 'retry'), + child: const Text('Retry'), + ), + ], + ), + ); + + if (action == 'delete' && context.mounted) { + await downloadProvider.deleteDownload( + globalKey, + ); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t.downloads.downloadDeleted, + ), + ), + ); + } + } else if (action == 'retry' && + context.mounted) { + final client = _getClientForMetadata( + context, + ); + if (client == null) return; + await downloadProvider.deleteDownload( + globalKey, + ); + try { + await downloadProvider.queueDownload( + metadata, + client, + ); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t.downloads.downloadQueued, + ), + ), + ); + } + } on CellularDownloadBlockedException { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t + .settings + .cellularDownloadBlocked, + ), + ), + ); + } + } + } + }, + icon: const Icon(Icons.cancel_outlined), + tooltip: 'Cancelled download', + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + foregroundColor: Colors.grey, + ), + ); + } + + // State 7: Partial Download (some episodes downloaded, not all) + if (progress?.status == DownloadStatus.partial) { + final currentFile = progress?.currentFile; + final tooltip = currentFile != null + ? 'Downloaded $currentFile - Click to complete' + : 'Partially downloaded - Click to complete'; + + return IconButton.filledTonal( + onPressed: () async { + final client = _getClientForMetadata( + context, + ); + if (client == null) return; + + // Queue only the missing episodes + final count = await downloadProvider + .queueMissingEpisodes(metadata, client); + + if (context.mounted) { + final message = count > 0 + ? t.downloads.episodesQueued( + count: count, + ) + : 'All episodes already downloaded'; + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar(content: Text(message)), + ); + } + }, + tooltip: tooltip, + icon: const Icon(Icons.downloading), + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + foregroundColor: Colors.orange, + ), + ); + } + + // State 8: Downloaded/Completed (can delete) + if (downloadProvider.isDownloaded(globalKey)) { + return IconButton.filledTonal( + onPressed: () async { + // Show delete download confirmation + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.downloads.deleteDownload), + content: Text( + t.downloads.deleteConfirm( + title: metadata.title, + ), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, false), + child: Text(t.common.cancel), + ), + TextButton( + onPressed: () => + Navigator.pop(context, true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: Text(t.common.delete), + ), + ], + ), + ); + + if (confirmed == true && context.mounted) { + await downloadProvider.deleteDownload( + globalKey, + ); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + t.downloads.downloadDeleted, + ), + ), + ); + } + } + }, + icon: const Icon(Icons.file_download_done), + tooltip: t.downloads.deleteDownload, + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + foregroundColor: Colors.green, + ), + ); + } + + // State 9: Not downloaded (default - can download) + return IconButton.filledTonal( + onPressed: () async { + final client = _getClientForMetadata(context); + if (client == null) return; + final count = await downloadProvider + .queueDownload(metadata, client); + if (context.mounted) { + final message = count > 1 + ? t.downloads.episodesQueued( + count: count, + ) + : t.downloads.downloadQueued; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + }, + icon: const Icon(Icons.download), + tooltip: t.downloads.downloadNow, + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + ), + ); + }, + ), + const SizedBox(width: 12), + // Mark as watched/unwatched (works offline too) IconButton.filledTonal( onPressed: () async { try { - final client = _getClientForMetadata(context); - - await client.markAsWatched(metadata.ratingKey); - if (context.mounted) { - _watchStateChanged = true; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(t.messages.markedAsWatched), - ), + if (widget.isOffline) { + // Offline mode: queue action for later sync + final offlineWatch = context + .read(); + await offlineWatch.markAsWatched( + serverId: metadata.serverId!, + ratingKey: metadata.ratingKey, ); - // Update watch state without full rebuild - _updateWatchState(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + t.messages.markedAsWatchedOffline, + ), + ), + ); + // Refresh offline OnDeck + _loadOfflineOnDeckEpisode(); + } + } else { + // Online mode: send to server + final client = _getClientForMetadata(context); + if (client == null) return; + + await client.markAsWatched(metadata.ratingKey); + if (context.mounted) { + _watchStateChanged = true; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(t.messages.markedAsWatched), + ), + ); + // Update watch state without full rebuild + _updateWatchState(); + } } } catch (e) { if (context.mounted) { @@ -848,18 +1515,45 @@ class _MediaDetailScreenState extends State { IconButton.filledTonal( onPressed: () async { try { - final client = _getClientForMetadata(context); - - await client.markAsUnwatched(metadata.ratingKey); - if (context.mounted) { - _watchStateChanged = true; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(t.messages.markedAsUnwatched), - ), + if (widget.isOffline) { + // Offline mode: queue action for later sync + final offlineWatch = context + .read(); + await offlineWatch.markAsUnwatched( + serverId: metadata.serverId!, + ratingKey: metadata.ratingKey, ); - // Update watch state without full rebuild - _updateWatchState(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + t.messages.markedAsUnwatchedOffline, + ), + ), + ); + // Refresh offline OnDeck + _loadOfflineOnDeckEpisode(); + } + } else { + // Online mode: send to server + final client = _getClientForMetadata(context); + if (client == null) return; + + await client.markAsUnwatched( + metadata.ratingKey, + ); + if (context.mounted) { + _watchStateChanged = true; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + t.messages.markedAsUnwatched, + ), + ), + ); + // Update watch state without full rebuild + _updateWatchState(); + } } } catch (e) { if (context.mounted) { @@ -943,16 +1637,31 @@ class _MediaDetailScreenState extends State { const SizedBox(height: 12), itemBuilder: (context, index) { final season = _seasons[index]; + // Look up each season's artwork, not the show's + String? seasonPosterPath; + if (widget.isOffline && season.serverId != null) { + seasonPosterPath = context + .read() + .getArtworkLocalPath( + season.serverId!, + season.thumb, + ); + } return _SeasonCard( season: season, client: _getClientForMetadata(context), + isOffline: widget.isOffline, + localPosterPath: seasonPosterPath, onTap: () async { final watchStateChanged = await Navigator.push( context, MaterialPageRoute( builder: (context) => - SeasonDetailScreen(season: season), + SeasonDetailScreen( + season: season, + isOffline: widget.isOffline, + ), ), ); if (watchStateChanged == true) { @@ -1133,52 +1842,66 @@ class _MediaDetailScreenState extends State { } String _getPlayButtonLabel(PlexMetadata metadata) { - // For TV shows + // For TV shows - use compact S1E1 format if (metadata.type.toLowerCase() == 'show') { if (_onDeckEpisode != null) { final episode = _onDeckEpisode!; final seasonNum = episode.parentIndex ?? 0; final episodeNum = episode.index ?? 0; - // Check if episode has been partially watched (viewOffset > 0) - if (episode.viewOffset != null && episode.viewOffset! > 0) { - return t.discover.resumeEpisode( - season: seasonNum.toString(), - episode: episodeNum.toString(), - ); - } else { - return t.discover.playEpisode( - season: seasonNum.toString(), - episode: episodeNum.toString(), - ); - } + // Use the same format for both play and resume + // (icon will indicate the difference) + return t.discover.playEpisode( + season: seasonNum.toString(), + episode: episodeNum.toString(), + ); } else { // No on deck episode, will play first episode return t.discover.playEpisode(season: '1', episode: '1'); } } - // For movies or episodes, check if partially watched - if (metadata.viewOffset != null && metadata.viewOffset! > 0) { - return t.discover.resume; + // For movies or episodes - NO TEXT, just icon + return ''; + } + + IconData _getPlayButtonIcon(PlexMetadata metadata) { + // For TV shows + if (metadata.type.toLowerCase() == 'show') { + if (_onDeckEpisode != null) { + final episode = _onDeckEpisode!; + // Check if episode has been partially watched + if (episode.viewOffset != null && episode.viewOffset! > 0) { + return Icons.restore; // Resume icon + } + } + } else { + // For movies or episodes + if (metadata.viewOffset != null && metadata.viewOffset! > 0) { + return Icons.restore; // Resume icon + } } - return t.discover.play; + return Icons.play_arrow; // Default play icon } } /// Season card widget class _SeasonCard extends StatelessWidget { final PlexMetadata season; - final PlexClient client; + final PlexClient? client; final VoidCallback onTap; final VoidCallback onRefresh; + final bool isOffline; + final String? localPosterPath; const _SeasonCard({ required this.season, - required this.client, + this.client, required this.onTap, required this.onRefresh, + this.isOffline = false, + this.localPosterPath, }); @override @@ -1201,44 +1924,56 @@ class _SeasonCard extends StatelessWidget { child: Row( children: [ // Season poster - if (season.thumb != null) - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: PlexOptimizedImage.poster( - client: client, - imagePath: season.thumb, - width: 80, - height: 120, - fit: BoxFit.cover, - placeholder: (context, url) => Container( - width: 80, - height: 120, - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - ), - errorWidget: (context, url, error) => Container( - width: 80, - height: 120, - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - child: const Icon(Icons.movie, size: 32), - ), - ), - ) - else - Container( - width: 80, - height: 120, - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(6), - ), - child: const Icon(Icons.movie, size: 32), - ), + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: isOffline && localPosterPath != null + ? Image.file( + File(localPosterPath!), + width: 80, + height: 120, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container( + width: 80, + height: 120, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie, size: 32), + ), + ) + : season.thumb != null + ? PlexOptimizedImage.poster( + client: client, + imagePath: season.thumb, + width: 80, + height: 120, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 80, + height: 120, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + ), + errorWidget: (context, url, error) => Container( + width: 80, + height: 120, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie, size: 32), + ), + ) + : Container( + width: 80, + height: 120, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie, size: 32), + ), + ), const SizedBox(width: 16), // Season info @@ -1260,39 +1995,42 @@ class _SeasonCard extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium ?.copyWith(color: Colors.grey), ), - const SizedBox(height: 8), - if (season.viewedLeafCount != null && - season.leafCount != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: - season.viewedLeafCount! / - season.leafCount!, - backgroundColor: tokens(context).outline, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary, + // Hide watch progress when offline (not tracked) + if (!isOffline) ...[ + const SizedBox(height: 8), + if (season.viewedLeafCount != null && + season.leafCount != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: + season.viewedLeafCount! / + season.leafCount!, + backgroundColor: tokens(context).outline, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + minHeight: 6, ), - minHeight: 6, ), ), - ), - const SizedBox(height: 4), - Text( - t.discover.watchedProgress( - watched: season.viewedLeafCount.toString(), - total: season.leafCount.toString(), + const SizedBox(height: 4), + Text( + t.discover.watchedProgress( + watched: season.viewedLeafCount.toString(), + total: season.leafCount.toString(), + ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: Colors.grey), ), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: Colors.grey), - ), - ], - ), + ], + ), + ], ], ), ), diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index b82c00b6..906c1d09 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -1,7 +1,13 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../../services/plex_client.dart'; import '../focus/key_event_utils.dart'; import '../focus/input_mode_tracker.dart'; +import '../models/download_status.dart'; +import '../providers/download_provider.dart'; +import '../services/download_storage_service.dart'; import '../widgets/plex_optimized_image.dart'; import '../models/plex_metadata.dart'; import '../utils/provider_extensions.dart'; @@ -15,8 +21,13 @@ import '../i18n/strings.g.dart'; class SeasonDetailScreen extends StatefulWidget { final PlexMetadata season; + final bool isOffline; - const SeasonDetailScreen({super.key, required this.season}); + const SeasonDetailScreen({ + super.key, + required this.season, + this.isOffline = false, + }); @override State createState() => _SeasonDetailScreenState(); @@ -24,17 +35,20 @@ class SeasonDetailScreen extends StatefulWidget { class _SeasonDetailScreenState extends State with ItemUpdatable { - late final PlexClient _client; + PlexClient? _client; @override - PlexClient get client => _client; + PlexClient get client => _client!; List _episodes = []; bool _isLoadingEpisodes = false; bool _watchStateChanged = false; /// Get the correct PlexClient for this season's server - PlexClient _getClientForSeason(BuildContext context) { + PlexClient? _getClientForSeason(BuildContext context) { + if (widget.isOffline || widget.season.serverId == null) { + return null; + } return context.getClientForServer(widget.season.serverId!); } @@ -53,9 +67,15 @@ class _SeasonDetailScreenState extends State _isLoadingEpisodes = true; }); + if (widget.isOffline) { + // Load episodes from downloads + _loadEpisodesFromDownloads(); + return; + } + try { // Episodes are automatically tagged with server info by PlexClient - final episodes = await _client.getChildren(widget.season.ratingKey); + final episodes = await _client!.getChildren(widget.season.ratingKey); setState(() { _episodes = episodes; @@ -68,6 +88,28 @@ class _SeasonDetailScreenState extends State } } + /// Load episodes from downloaded content + void _loadEpisodesFromDownloads() { + final downloadProvider = context.read(); + + // Get all downloaded episodes for the show (grandparentRatingKey) + final allEpisodes = downloadProvider.getDownloadedEpisodesForShow( + widget.season.parentRatingKey ?? '', + ); + + // Filter to only this season's episodes + final seasonEpisodes = + allEpisodes + .where((ep) => ep.parentIndex == widget.season.index) + .toList() + ..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0)); + + setState(() { + _episodes = seasonEpisodes; + _isLoadingEpisodes = false; + }); + } + @override Future updateItem(String ratingKey) async { _watchStateChanged = true; @@ -125,17 +167,40 @@ class _SeasonDetailScreenState extends State SliverList( delegate: SliverChildBuilderDelegate((context, index) { final episode = _episodes[index]; + // Get local poster path for offline mode + String? localPosterPath; + if (widget.isOffline && episode.serverId != null) { + final downloadProvider = context.read(); + final globalKey = + '${episode.serverId}:${episode.ratingKey}'; + // Get the artwork reference and convert to local file path + final artworkRef = downloadProvider.getArtworkPaths( + globalKey, + ); + localPosterPath = artworkRef?.getLocalPath( + DownloadStorageService.instance, + episode.serverId!, + ); + } return _EpisodeCard( episode: episode, client: _client, + isOffline: widget.isOffline, + localPosterPath: localPosterPath, autofocus: index == 0 && InputModeTracker.isKeyboardMode(context), onTap: () async { - await navigateToVideoPlayer(context, metadata: episode); - // Refresh episodes when returning from video player - _loadEpisodes(); + await navigateToVideoPlayer( + context, + metadata: episode, + isOffline: widget.isOffline, + ); + // Refresh episodes when returning from video player (skip if offline) + if (!widget.isOffline) { + _loadEpisodes(); + } }, - onRefresh: updateItem, + onRefresh: widget.isOffline ? null : updateItem, ); }, childCount: _episodes.length), ), @@ -149,17 +214,21 @@ class _SeasonDetailScreenState extends State /// Episode card widget class _EpisodeCard extends StatelessWidget { final PlexMetadata episode; - final PlexClient client; + final PlexClient? client; final VoidCallback onTap; - final Future Function(String) onRefresh; + final Future Function(String)? onRefresh; final bool autofocus; + final bool isOffline; + final String? localPosterPath; const _EpisodeCard({ required this.episode, - required this.client, + this.client, required this.onTap, - required this.onRefresh, + this.onRefresh, this.autofocus = false, + this.isOffline = false, + this.localPosterPath, }); Widget _buildEpisodeMetaRow(BuildContext context) { @@ -173,7 +242,8 @@ class _EpisodeCard extends StatelessWidget { fontSize: 12, ), ), - if (episode.duration != null && episode.isWatched) ...[ + // Hide watch status when offline (not tracked) + if (!isOffline && episode.duration != null && episode.isWatched) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: Text( @@ -198,7 +268,9 @@ class _EpisodeCard extends StatelessWidget { @override Widget build(BuildContext context) { + // Hide progress when offline (not tracked) final hasProgress = + !isOffline && episode.viewOffset != null && episode.duration != null && episode.viewOffset! > 0; @@ -236,7 +308,19 @@ class _EpisodeCard extends StatelessWidget { borderRadius: BorderRadius.circular(6), child: AspectRatio( aspectRatio: 16 / 9, - child: episode.thumb != null + child: isOffline && localPosterPath != null + ? Image.file( + File(localPosterPath!), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie, size: 32), + ), + ) + : episode.thumb != null ? PlexOptimizedImage.thumb( client: client, imagePath: episode.thumb, @@ -323,43 +407,161 @@ class _EpisodeCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Episode number and title - Row( - children: [ - if (episode.index != null) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, - ), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(3), - ), - child: Text( - 'E${episode.index}', - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - fontSize: 11, - fontWeight: FontWeight.w600, + // Episode number and title with download status + Consumer( + builder: (context, downloadProvider, _) { + // Build download status icon based on state + Widget? downloadStatusIcon; + + // Only show download status in online mode + if (!isOffline && episode.serverId != null) { + final globalKey = + '${episode.serverId}:${episode.ratingKey}'; + final progress = downloadProvider.getProgress( + globalKey, + ); + final isQueueing = downloadProvider.isQueueing( + globalKey, + ); + + // Helper to get status-specific muted color + Color getMutedColor(Color baseColor) { + return Color.lerp( + tokens(context).textMuted, + baseColor, + 0.3, // 30% of the status color, 70% muted + )!; + } + + if (isQueueing) { + // Queueing state - building queue + downloadStatusIcon = SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: tokens(context).textMuted, + ), + ); + } else if (progress?.status == + DownloadStatus.queued) { + // Queued state - waiting to download + downloadStatusIcon = Icon( + Icons.schedule, + size: 12, + color: getMutedColor(Colors.orange), + ); + } else if (progress?.status == + DownloadStatus.downloading) { + // Downloading state - active download with radial progress + downloadStatusIcon = SizedBox( + width: 14, + height: 14, + child: Stack( + alignment: Alignment.center, + children: [ + // Background circle + CircularProgressIndicator( + value: 1.0, + strokeWidth: 1.5, + valueColor: AlwaysStoppedAnimation( + getMutedColor( + Colors.blue, + ).withOpacity(0.3), + ), + ), + // Progress circle + CircularProgressIndicator( + value: progress?.progressPercent, + strokeWidth: 1.5, + valueColor: AlwaysStoppedAnimation( + getMutedColor(Colors.blue), + ), + ), + ], + ), + ); + } else if (progress?.status == + DownloadStatus.paused) { + // Paused state - download paused + downloadStatusIcon = Icon( + Icons.pause_circle_outline, + size: 12, + color: getMutedColor(Colors.amber), + ); + } else if (progress?.status == + DownloadStatus.failed) { + // Failed state - download failed + downloadStatusIcon = Icon( + Icons.error_outline, + size: 12, + color: getMutedColor(Colors.red), + ); + } else if (progress?.status == + DownloadStatus.cancelled) { + // Cancelled state - download cancelled + downloadStatusIcon = Icon( + Icons.cancel_outlined, + size: 12, + color: getMutedColor(Colors.grey), + ); + } else if (progress?.status == + DownloadStatus.completed) { + // Completed state - download complete + downloadStatusIcon = Icon( + Icons.file_download_done, + size: 12, + color: getMutedColor(Colors.green), + ); + } + // Note: No icon shown if not downloaded (null) + } + + return Row( + children: [ + // Episode number badge + if (episode.index != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'E${episode.index}', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ), + // Download status icon (if present) + if (downloadStatusIcon != null) ...[ + const SizedBox(width: 6), + downloadStatusIcon, + ], + const SizedBox(width: 8), + // Episode title + Expanded( + child: Text( + episode.title, + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - episode.title, - style: Theme.of(context).textTheme.titleSmall - ?.copyWith(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], + ], + ); + }, ), // Summary diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index d8020309..8d58e116 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../i18n/strings.g.dart'; +import '../../services/download_storage_service.dart'; +import '../../services/saf_storage_service.dart'; import '../../providers/settings_provider.dart'; import '../../providers/theme_provider.dart'; import '../../services/keyboard_shortcuts_service.dart'; @@ -37,6 +42,7 @@ class _SettingsScreenState extends State { bool _autoSkipIntro = true; bool _autoSkipCredits = true; int _autoSkipDelay = 5; + bool _downloadOnWifiOnly = false; // Update checking state bool _isCheckingForUpdate = false; @@ -63,6 +69,7 @@ class _SettingsScreenState extends State { _autoSkipIntro = _settingsService.getAutoSkipIntro(); _autoSkipCredits = _settingsService.getAutoSkipCredits(); _autoSkipDelay = _settingsService.getAutoSkipDelay(); + _downloadOnWifiOnly = _settingsService.getDownloadOnWifiOnly(); _isLoading = false; }); } @@ -85,6 +92,8 @@ class _SettingsScreenState extends State { const SizedBox(height: 24), _buildVideoPlaybackSection(), const SizedBox(height: 24), + _buildDownloadsSection(), + const SizedBox(height: 24), _buildKeyboardShortcutsSection(), const SizedBox(height: 24), _buildAdvancedSection(), @@ -333,6 +342,183 @@ class _SettingsScreenState extends State { ); } + Widget _buildDownloadsSection() { + final storageService = DownloadStorageService.instance; + final isCustom = storageService.isUsingCustomPath(); + + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.settings.downloads, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + // Download location picker - not available on iOS + if (!Platform.isIOS) + FutureBuilder( + future: storageService.getCurrentDownloadPathDisplay(), + builder: (context, snapshot) { + final currentPath = snapshot.data ?? '...'; + + return ListTile( + leading: const Icon(Icons.folder), + title: Text( + isCustom + ? t.settings.downloadLocationCustom + : t.settings.downloadLocationDefault, + ), + subtitle: Text( + currentPath, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showDownloadLocationDialog(), + ); + }, + ), + SwitchListTile( + secondary: const Icon(Icons.wifi), + title: Text(t.settings.downloadOnWifiOnly), + subtitle: Text(t.settings.downloadOnWifiOnlyDescription), + value: _downloadOnWifiOnly, + onChanged: (value) async { + setState(() => _downloadOnWifiOnly = value); + await _settingsService.setDownloadOnWifiOnly(value); + }, + ), + ], + ), + ); + } + + Future _showDownloadLocationDialog() async { + final storageService = DownloadStorageService.instance; + final isCustom = storageService.isUsingCustomPath(); + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(t.settings.downloads), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.settings.downloadLocationDescription), + const SizedBox(height: 16), + FutureBuilder( + future: storageService.getCurrentDownloadPathDisplay(), + builder: (context, snapshot) { + return Text( + t.settings.currentPath(path: snapshot.data ?? '...'), + style: Theme.of(context).textTheme.bodySmall, + ); + }, + ), + ], + ), + actions: [ + if (isCustom) + TextButton( + onPressed: () async { + Navigator.pop(dialogContext); + await _resetDownloadLocation(); + }, + child: Text(t.settings.resetToDefault), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(t.common.cancel), + ), + FilledButton( + onPressed: () async { + Navigator.pop(dialogContext); + await _selectDownloadLocation(); + }, + child: Text(t.settings.selectFolder), + ), + ], + ), + ); + } + + Future _selectDownloadLocation() async { + try { + String? selectedPath; + String pathType = 'file'; + + if (Platform.isAndroid) { + // Use SAF on Android + final safService = SafStorageService.instance; + selectedPath = await safService.pickDirectory(); + if (selectedPath != null) { + pathType = 'saf'; + } + } else { + // Use file_picker on desktop + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: t.settings.selectFolder, + ); + selectedPath = result; + } + + if (selectedPath != null) { + // Validate the path is writable (for non-SAF paths) + if (pathType == 'file') { + final dir = Directory(selectedPath); + final isWritable = await DownloadStorageService.instance + .isDirectoryWritable(dir); + if (!isWritable) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.settings.downloadLocationInvalid)), + ); + } + return; + } + } + + // Save the setting + await _settingsService.setCustomDownloadPath( + selectedPath, + type: pathType, + ); + await DownloadStorageService.instance.refreshCustomPath(); + + if (mounted) { + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.settings.downloadLocationChanged)), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.settings.downloadLocationSelectError)), + ); + } + } + } + + Future _resetDownloadLocation() async { + await _settingsService.setCustomDownloadPath(null); + await DownloadStorageService.instance.refreshCustomPath(); + + if (mounted) { + setState(() {}); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(t.settings.downloadLocationReset))); + } + } + Widget _buildKeyboardShortcutsSection() { return Card( child: Column( diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index dbf0e7d8..c47813bf 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -10,14 +10,17 @@ import 'package:wakelock_plus/wakelock_plus.dart'; import '../mpv/mpv.dart'; import '../../services/plex_client.dart'; +import '../services/plex_api_cache.dart'; import '../models/plex_media_version.dart'; import '../models/plex_metadata.dart'; import '../models/plex_media_info.dart'; +import '../providers/download_provider.dart'; import '../providers/playback_state_provider.dart'; import '../services/episode_navigation_service.dart'; import '../services/media_controls_manager.dart'; import '../services/playback_initialization_service.dart'; import '../services/playback_progress_tracker.dart'; +import '../services/offline_watch_sync_service.dart'; import '../services/settings_service.dart'; import '../services/track_selection_service.dart'; import '../services/video_filter_manager.dart'; @@ -37,6 +40,7 @@ class VideoPlayerScreen extends StatefulWidget { final SubtitleTrack? preferredSubtitleTrack; final double? preferredPlaybackRate; final int selectedMediaIndex; + final bool isOffline; const VideoPlayerScreen({ super.key, @@ -45,6 +49,7 @@ class VideoPlayerScreen extends StatefulWidget { this.preferredSubtitleTrack, this.preferredPlaybackRate, this.selectedMediaIndex = 0, + this.isOffline = false, }); @override @@ -193,9 +198,9 @@ class VideoPlayerScreenState extends State // Re-enable wakelock since we're back in the video player WakelockPlus.enable(); - // Restore media metadata - final client = _getClientForMetadata(context); - if (_mediaControlsManager != null) { + // Restore media metadata (only in online mode - requires client for artwork URLs) + if (!widget.isOffline && _mediaControlsManager != null) { + final client = _getClientForMetadata(context); _mediaControlsManager!.updateMetadata( metadata: widget.metadata, client: client, @@ -213,7 +218,9 @@ class VideoPlayerScreenState extends State } _updateMediaControlsPlaybackState(); - appLogger.d('Media controls restored and wakelock re-enabled on app resume'); + appLogger.d( + 'Media controls restored and wakelock re-enabled on app resume', + ); } break; case AppLifecycleState.detached: @@ -458,15 +465,30 @@ class VideoPlayerScreenState extends State Future _initializeServices() async { if (!mounted || player == null) return; - final client = _getClientForMetadata(context); + // Get client (null in offline mode) + final client = widget.isOffline ? null : _getClientForMetadata(context); // Initialize progress tracker - _progressTracker = PlaybackProgressTracker( - client: client, - metadata: widget.metadata, - player: player!, - ); - _progressTracker!.startTracking(); + if (widget.isOffline) { + // Offline mode: queue progress updates for later sync + final offlineWatchService = context.read(); + _progressTracker = PlaybackProgressTracker( + client: null, + metadata: widget.metadata, + player: player!, + isOffline: true, + offlineWatchService: offlineWatchService, + ); + _progressTracker!.startTracking(); + } else if (client != null) { + // Online mode: send progress to server + _progressTracker = PlaybackProgressTracker( + client: client, + metadata: widget.metadata, + player: player!, + ); + _progressTracker!.startTracking(); + } // Initialize media controls manager _mediaControlsManager = MediaControlsManager(); @@ -508,7 +530,7 @@ class VideoPlayerScreenState extends State } }); - // Update media metadata + // Update media metadata (client can be null in offline mode - artwork won't be shown) await _mediaControlsManager!.updateMetadata( metadata: widget.metadata, client: client, @@ -548,6 +570,9 @@ class VideoPlayerScreenState extends State Future _ensurePlayQueue() async { if (!mounted) return; + // Skip play queue in offline mode (requires server connection) + if (widget.isOffline) return; + // Only create play queues for episodes if (widget.metadata.type.toLowerCase() != 'episode') { return; @@ -618,6 +643,9 @@ class VideoPlayerScreenState extends State Future _loadAdjacentEpisodes() async { if (!mounted) return; + // Skip loading adjacent episodes in offline mode (requires server connection) + if (widget.isOffline) return; + try { // Use server-specific client for this metadata final client = _getClientForMetadata(context); @@ -645,17 +673,24 @@ class VideoPlayerScreenState extends State if (!mounted) return; try { - // Use server-specific client for this metadata - final client = _getClientForMetadata(context); + PlaybackInitializationResult result; - // Initialize playback service - final playbackService = PlaybackInitializationService(client: client); - - // Get playback data (video URL and available versions) - final result = await playbackService.getPlaybackData( - metadata: widget.metadata, - selectedMediaIndex: widget.selectedMediaIndex, - ); + if (widget.isOffline) { + // Offline mode: get video path from downloads without requiring server + result = await _startOfflinePlayback(); + } else { + // Online mode: use server-specific client + final client = _getClientForMetadata(context); + final playbackService = PlaybackInitializationService( + client: client, + database: PlexApiCache.instance.database, + ); + result = await playbackService.getPlaybackData( + metadata: widget.metadata, + selectedMediaIndex: widget.selectedMediaIndex, + preferOffline: true, // Use downloaded file if available + ); + } // Open video through Player if (result.videoUrl != null) { @@ -674,7 +709,7 @@ class VideoPlayerScreenState extends State }); // Initialize video filter manager with player and available versions - if (player != null && _availableVersions.isNotEmpty) { + if (player != null) { _videoFilterManager = VideoFilterManager( player: player!, availableVersions: _availableVersions, @@ -718,6 +753,36 @@ class VideoPlayerScreenState extends State } } + /// Start playback for offline/downloaded content + Future _startOfflinePlayback() async { + final downloadProvider = context.read(); + + // Debug: log metadata info + appLogger.d( + 'Offline playback - serverId: ${widget.metadata.serverId}, ratingKey: ${widget.metadata.ratingKey}', + ); + + final globalKey = + '${widget.metadata.serverId}:${widget.metadata.ratingKey}'; + appLogger.d('Looking up video with globalKey: $globalKey'); + + final videoPath = await downloadProvider.getVideoFilePath(globalKey); + if (videoPath == null) { + appLogger.e('Video file path not found for globalKey: $globalKey'); + throw PlaybackException(t.messages.fileInfoNotAvailable); + } + + appLogger.d('Starting offline playback: $videoPath'); + + return PlaybackInitializationResult( + availableVersions: [], + videoUrl: 'file://$videoPath', + mediaInfo: null, + externalSubtitles: const [], + isOffline: true, + ); + } + /// Cycle through BoxFit modes: contain → cover → fill → contain (for button) void _cycleBoxFitMode() { setState(() { diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart new file mode 100644 index 00000000..102f5087 --- /dev/null +++ b/lib/services/download_manager_service.dart @@ -0,0 +1,1717 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:path/path.dart' as path; +import '../database/app_database.dart'; +import 'settings_service.dart'; +import '../models/download_status.dart'; +import '../models/download_progress.dart'; +import '../models/deletion_progress.dart'; +import '../models/plex_metadata.dart'; +import '../models/plex_media_info.dart'; +import '../services/plex_client.dart'; +import '../services/download_storage_service.dart'; +import '../services/plex_api_cache.dart'; +import '../utils/app_logger.dart'; + +/// Extension methods on AppDatabase for download operations +extension DownloadDatabaseOperations on AppDatabase { + /// Insert a new download into the database + Future insertDownload({ + required String serverId, + required String ratingKey, + required String globalKey, + required String type, + String? parentRatingKey, + String? grandparentRatingKey, + required int status, + }) async { + await into(downloadedMedia).insert( + DownloadedMediaCompanion.insert( + serverId: serverId, + ratingKey: ratingKey, + globalKey: globalKey, + type: type, + parentRatingKey: Value(parentRatingKey), + grandparentRatingKey: Value(grandparentRatingKey), + status: status, + ), + mode: InsertMode.insertOrReplace, + ); + } + + /// Add item to download queue + Future addToQueue({ + required String mediaGlobalKey, + int priority = 0, + bool downloadSubtitles = true, + bool downloadArtwork = true, + }) async { + await into(downloadQueue).insert( + DownloadQueueCompanion.insert( + mediaGlobalKey: mediaGlobalKey, + priority: Value(priority), + addedAt: DateTime.now().millisecondsSinceEpoch, + downloadSubtitles: Value(downloadSubtitles), + downloadArtwork: Value(downloadArtwork), + ), + mode: InsertMode.insertOrReplace, + ); + } + + /// Get next item from queue (highest priority, oldest first) + /// Only returns items that are not paused + Future getNextQueueItem() async { + // Join with downloadedMedia to check status and filter out paused items + final query = select(downloadQueue).join([ + innerJoin( + downloadedMedia, + downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey), + ), + ]); + + query + ..where(downloadedMedia.status.equals(DownloadStatus.paused.index).not()) + ..orderBy([ + OrderingTerm( + expression: downloadQueue.priority, + mode: OrderingMode.desc, + ), + OrderingTerm(expression: downloadQueue.addedAt), + ]) + ..limit(1); + + final result = await query.getSingleOrNull(); + return result?.readTable(downloadQueue); + } + + /// Update download status + Future updateDownloadStatus(String globalKey, int status) async { + await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))) + .write(DownloadedMediaCompanion(status: Value(status))); + } + + /// Update download progress + Future updateDownloadProgress( + String globalKey, + int progress, + int downloadedBytes, + int totalBytes, + ) async { + await (update( + downloadedMedia, + )..where((t) => t.globalKey.equals(globalKey))).write( + DownloadedMediaCompanion( + progress: Value(progress), + downloadedBytes: Value(downloadedBytes), + totalBytes: Value(totalBytes), + ), + ); + } + + /// Update video file path + Future updateVideoFilePath(String globalKey, String filePath) async { + await (update( + downloadedMedia, + )..where((t) => t.globalKey.equals(globalKey))).write( + DownloadedMediaCompanion( + videoFilePath: Value(filePath), + downloadedAt: Value(DateTime.now().millisecondsSinceEpoch), + ), + ); + } + + /// Update artwork paths + Future updateArtworkPaths({ + required String globalKey, + String? thumbPath, + }) async { + await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))) + .write(DownloadedMediaCompanion(thumbPath: Value(thumbPath))); + } + + /// Update download error and increment retry count + Future updateDownloadError( + String globalKey, + String errorMessage, + ) async { + // Get current retry count to increment it + final existing = await getDownloadedMedia(globalKey); + final currentCount = existing?.retryCount ?? 0; + + await (update( + downloadedMedia, + )..where((t) => t.globalKey.equals(globalKey))).write( + DownloadedMediaCompanion( + errorMessage: Value(errorMessage), + retryCount: Value(currentCount + 1), + ), + ); + } + + /// Clear download error and reset retry count (for retry) + Future clearDownloadError(String globalKey) async { + await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))) + .write(const DownloadedMediaCompanion( + errorMessage: Value(null), + retryCount: Value(0), + )); + } + + /// Remove item from queue + Future removeFromQueue(String mediaGlobalKey) async { + await (delete( + downloadQueue, + )..where((t) => t.mediaGlobalKey.equals(mediaGlobalKey))).go(); + } + + /// Get downloaded media item + Future getDownloadedMedia(String globalKey) async { + return (select( + downloadedMedia, + )..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull(); + } + + /// Delete a download + Future deleteDownload(String globalKey) async { + await (delete( + downloadedMedia, + )..where((t) => t.globalKey.equals(globalKey))).go(); + await (delete( + downloadQueue, + )..where((t) => t.mediaGlobalKey.equals(globalKey))).go(); + } +} + +class DownloadManagerService { + final AppDatabase _database; + final DownloadStorageService _storageService; + final PlexApiCache _apiCache = PlexApiCache.instance; + final Dio _dio; + + // Stream controller for download progress updates + final _progressController = StreamController.broadcast(); + Stream get progressStream => _progressController.stream; + + // Stream controller for deletion progress updates + final _deletionProgressController = + StreamController.broadcast(); + Stream get deletionProgressStream => + _deletionProgressController.stream; + + // Active downloads with cancel tokens + final Map _activeDownloads = {}; + + // Flag to prevent multiple queue processing + bool _isProcessingQueue = false; + + // Connectivity listener for auto-resume + StreamSubscription>? _connectivitySubscription; + + // Cached client for auto-resume + PlexClient? _lastClient; + + /// Check if downloads should be blocked due to cellular-only setting + Future _shouldBlockDownload() async { + return shouldBlockDownloadOnCellular(); + } + + /// Public method to check if downloads should be blocked due to cellular-only setting + /// Can be used by DownloadProvider to show user-friendly error + static Future shouldBlockDownloadOnCellular() async { + final settings = await SettingsService.getInstance(); + if (!settings.getDownloadOnWifiOnly()) return false; + + final connectivity = await Connectivity().checkConnectivity(); + // Block if on cellular and NOT on WiFi (allow if both are available) + return connectivity.contains(ConnectivityResult.mobile) && + !connectivity.contains(ConnectivityResult.wifi) && + !connectivity.contains(ConnectivityResult.ethernet); + } + + DownloadManagerService({ + required AppDatabase database, + required DownloadStorageService storageService, + Dio? dio, + }) : _database = database, + _storageService = storageService, + _dio = dio ?? Dio(); + + /// Queue a download for a media item + Future queueDownload({ + required PlexMetadata metadata, + required PlexClient client, + int priority = 0, + bool downloadSubtitles = true, + bool downloadArtwork = true, + }) async { + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + + // Check if already downloading or completed + final existing = await _database.getDownloadedMedia(globalKey); + if (existing != null && + (existing.status == DownloadStatus.downloading.index || + existing.status == DownloadStatus.completed.index)) { + appLogger.i( + 'Download already exists for $globalKey with status ${existing.status}', + ); + return; + } + + // Insert into database + await _database.insertDownload( + serverId: metadata.serverId!, + ratingKey: metadata.ratingKey, + globalKey: globalKey, + type: metadata.type, + parentRatingKey: metadata.parentRatingKey, + grandparentRatingKey: metadata.grandparentRatingKey, + status: DownloadStatus.queued.index, + ); + + // Pin the already-cached API response for offline use + // (getMetadataWithImages was already called by download_provider, which cached with chapters/markers) + await _apiCache.pinForOffline(metadata.serverId!, metadata.ratingKey); + + // Add to queue + await _database.addToQueue( + mediaGlobalKey: globalKey, + priority: priority, + downloadSubtitles: downloadSubtitles, + downloadArtwork: downloadArtwork, + ); + + _emitProgress(globalKey, DownloadStatus.queued, 0); + + // Start processing if not already + _processQueue(client); + } + + /// Start processing the download queue - processes one item at a time + Future _processQueue(PlexClient client) async { + if (_isProcessingQueue) { + appLogger.d('Queue processing already in progress'); + return; + } + + _isProcessingQueue = true; + _lastClient = client; // Cache for auto-resume + _setupConnectivityListener(); // Setup listener for auto-resume + + try { + while (true) { + // Check if we should pause due to cellular + if (await _shouldBlockDownload()) { + appLogger.i( + 'Pausing downloads - on cellular data with WiFi-only enabled', + ); + break; + } + + // Get next item from queue + final nextItem = await _database.getNextQueueItem(); + if (nextItem == null) { + appLogger.d('No more items in queue'); + break; + } + + await _startDownload(nextItem.mediaGlobalKey, client, nextItem); + } + } finally { + _isProcessingQueue = false; + } + } + + /// Setup connectivity listener to auto-resume downloads when WiFi becomes available + void _setupConnectivityListener() { + _connectivitySubscription?.cancel(); + _connectivitySubscription = Connectivity().onConnectivityChanged.listen(( + results, + ) async { + // If WiFi becomes available, try to resume queue + if (results.contains(ConnectivityResult.wifi) || + results.contains(ConnectivityResult.ethernet)) { + final hasQueuedItems = await _database.getNextQueueItem() != null; + if (hasQueuedItems && !_isProcessingQueue && _lastClient != null) { + appLogger.i('WiFi available - resuming downloads'); + _processQueue(_lastClient!); + } + } + }); + } + + /// Start downloading a specific item + Future _startDownload( + String globalKey, + PlexClient client, + DownloadQueueItem queueItem, + ) async { + try { + appLogger.i('Starting download for $globalKey'); + + // Update status to downloading + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.downloading.index, + ); + appLogger.d('Status updated to downloading, emitting initial progress'); + _emitProgress(globalKey, DownloadStatus.downloading, 0); + + // Parse globalKey to get serverId and ratingKey + final parts = globalKey.split(':'); + final serverId = parts[0]; + final ratingKey = parts[1]; + + // Get metadata from cache + final cachedResponse = await _apiCache.get( + serverId, + '/library/metadata/$ratingKey', + ); + if (cachedResponse == null) { + throw Exception('Metadata not found in cache for $globalKey'); + } + + // Parse metadata from cached response + final metadataList = + cachedResponse['MediaContainer']?['Metadata'] as List?; + if (metadataList == null || metadataList.isEmpty) { + throw Exception('Invalid cached metadata for $globalKey'); + } + final metadata = PlexMetadata.fromJson( + metadataList[0], + ).copyWith(serverId: serverId); + + // Get video playback data (includes URL, streams, etc.) + final playbackData = await client.getVideoPlaybackData( + metadata.ratingKey, + ); + if (playbackData.videoUrl == null) { + throw Exception('Could not get video URL'); + } + + // Cache playback extras (chapters + markers) for offline use + // This fetches and caches in one call + await client.getPlaybackExtras(metadata.ratingKey); + + // Determine file extension from URL or default to mp4 + final extension = _getExtensionFromUrl(playbackData.videoUrl!) ?? 'mp4'; + + final metadataWithServer = metadata; + + // For episodes, look up the show's year from cached show metadata + int? showYear; + if (metadataWithServer.type == 'episode' && + metadataWithServer.grandparentRatingKey != null) { + final showCached = await _apiCache.get( + serverId, + '/library/metadata/${metadataWithServer.grandparentRatingKey}', + ); + if (showCached != null) { + final showList = showCached['MediaContainer']?['Metadata'] as List?; + if (showList != null && showList.isNotEmpty) { + final showMetadata = PlexMetadata.fromJson(showList[0]); + showYear = showMetadata.year; + } + } + } + + // Create cancel token + final cancelToken = CancelToken(); + _activeDownloads[globalKey] = cancelToken; + + appLogger.d('Starting video download for $globalKey'); + + // Determine download path and handle SAF mode + final String downloadFilePath; + final String storedPath; + + if (_storageService.isUsingSaf) { + // SAF mode: download to temp cache first, then copy to SAF + final tempFileName = '${globalKey.replaceAll(':', '_')}.$extension'; + downloadFilePath = await _storageService.getTempDownloadPath( + tempFileName, + ); + + // Download to temp path + await _downloadFile( + url: playbackData.videoUrl!, + filePath: downloadFilePath, + globalKey: globalKey, + cancelToken: cancelToken, + ); + + appLogger.d('Video downloaded to temp, copying to SAF for $globalKey'); + + // Copy to SAF + final List pathComponents; + final String safFileName; + if (metadataWithServer.type == 'movie') { + pathComponents = _storageService.getMovieSafPathComponents( + metadataWithServer, + ); + safFileName = _storageService.getMovieSafFileName( + metadataWithServer, + extension, + ); + } else if (metadataWithServer.type == 'episode') { + pathComponents = _storageService.getEpisodeSafPathComponents( + metadataWithServer, + showYear: showYear, + ); + safFileName = _storageService.getEpisodeSafFileName( + metadataWithServer, + extension, + ); + } else { + pathComponents = [serverId, metadataWithServer.ratingKey]; + safFileName = 'video.$extension'; + } + + final safUri = await _storageService.copyToSaf( + downloadFilePath, + pathComponents, + safFileName, + _storageService.getMimeType(extension), + ); + + if (safUri == null) { + throw Exception('Failed to copy video to SAF storage'); + } + + storedPath = safUri; + appLogger.d('Video copied to SAF: $safUri'); + } else { + // Normal mode: download directly to final path + if (metadataWithServer.type == 'movie') { + downloadFilePath = await _storageService.getMovieVideoPath( + metadataWithServer, + extension, + ); + } else if (metadataWithServer.type == 'episode') { + downloadFilePath = await _storageService.getEpisodeVideoPath( + metadataWithServer, + extension, + showYear: showYear, + ); + } else { + downloadFilePath = await _storageService.getVideoFilePath( + serverId, + metadataWithServer.ratingKey, + extension, + ); + } + + await _downloadFile( + url: playbackData.videoUrl!, + filePath: downloadFilePath, + globalKey: globalKey, + cancelToken: cancelToken, + ); + + // Store relative path (survives iOS container UUID changes) + storedPath = await _storageService.toRelativePath(downloadFilePath); + } + + appLogger.d('Video download completed for $globalKey'); + + // Update database with stored path (SAF URI or relative path) + await _database.updateVideoFilePath(globalKey, storedPath); + + // Download artwork if enabled (only episode-specific artwork, not show/season) + // Use the passed queueItem's settings (not getNextQueueItem which would return the NEXT item) + if (queueItem.downloadArtwork) { + await _downloadArtwork( + globalKey, + metadataWithServer, + client, + showYear: showYear, + ); + + // Download chapter thumbnails + await _downloadChapterThumbnails( + metadataWithServer.serverId!, + metadataWithServer.ratingKey, + client, + ); + } + + // Download subtitles if enabled + if (queueItem.downloadSubtitles && playbackData.mediaInfo != null) { + await _downloadSubtitles( + globalKey, + metadataWithServer, + playbackData.mediaInfo!, + client, + showYear: showYear, + ); + } + + // Mark as completed + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.completed.index, + ); + await _database.removeFromQueue(globalKey); + _emitProgress(globalKey, DownloadStatus.completed, 100); + + _activeDownloads.remove(globalKey); + + appLogger.i('Download completed for $globalKey'); + } catch (e) { + // Check if this was a user-initiated cancel/pause (not a real failure) + if (e is DioException && e.type == DioExceptionType.cancel) { + // Status was already set by pauseDownload() or cancelDownload() + // Just clean up and exit without marking as failed + appLogger.d('Download cancelled/paused for $globalKey: ${e.message}'); + _activeDownloads.remove(globalKey); + return; + } + + appLogger.e('Download failed for $globalKey', error: e); + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.failed.index, + ); + await _database.updateDownloadError(globalKey, e.toString()); + // Remove from queue to prevent endless retry loop + await _database.removeFromQueue(globalKey); + _emitProgress( + globalKey, + DownloadStatus.failed, + 0, + errorMessage: e.toString(), + ); + _activeDownloads.remove(globalKey); + } + } + + Future _downloadFile({ + required String url, + required String filePath, + required String globalKey, + required CancelToken cancelToken, + }) async { + final file = File(filePath); + await file.parent.create(recursive: true); + + int lastBytes = 0; + DateTime lastUpdate = DateTime.now(); + + await _dio.download( + url, + filePath, + cancelToken: cancelToken, + onReceiveProgress: (received, total) { + // Calculate speed (update every 500ms) + final now = DateTime.now(); + if (now.difference(lastUpdate).inMilliseconds >= 500) { + final elapsedSeconds = now + .difference(lastUpdate) + .inSeconds + .clamp(1, double.infinity); + final bytesPerSecond = (received - lastBytes) / elapsedSeconds; + lastUpdate = now; + lastBytes = received; + + final progress = total > 0 ? ((received / total) * 100).round() : 0; + + appLogger.d( + 'Download progress: $progress% ($received/$total bytes) for $globalKey', + ); + + _progressController.add( + DownloadProgress( + globalKey: globalKey, + status: DownloadStatus.downloading, + progress: progress, + downloadedBytes: received, + totalBytes: total, + speed: bytesPerSecond, + currentFile: 'video', + ), + ); + + // Update database asynchronously (non-blocking) + _database + .updateDownloadProgress(globalKey, progress, received, total) + .catchError((e) { + appLogger.w( + 'Failed to update download progress in DB', + error: e, + ); + }); + } + }, + ); + } + + /// Download artwork for a media item using hash-based storage + /// Downloads all artwork types: thumb/poster, clearLogo, and background art + Future _downloadArtwork( + String globalKey, + PlexMetadata metadata, + PlexClient client, { + int? showYear, + }) async { + if (metadata.serverId == null) return; + + try { + _emitProgress( + globalKey, + DownloadStatus.downloading, + 0, + currentFile: 'artwork', + ); + + final serverId = metadata.serverId!; + + // Download thumb/poster + if (metadata.thumb != null) { + await _downloadSingleArtwork(serverId, metadata.thumb!, client); + } + + // Download clear logo + if (metadata.clearLogo != null) { + await _downloadSingleArtwork(serverId, metadata.clearLogo!, client); + } + + // Download background art + if (metadata.art != null) { + await _downloadSingleArtwork(serverId, metadata.art!, client); + } + + // Store thumb reference in database (primary artwork for display) + await _database.updateArtworkPaths( + globalKey: globalKey, + thumbPath: metadata.thumb, + ); + + _emitProgressWithArtwork(globalKey, thumbPath: metadata.thumb); + appLogger.d('Artwork downloaded for $globalKey'); + } catch (e) { + appLogger.w('Failed to download artwork for $globalKey', error: e); + // Don't fail the entire download if artwork fails + } + } + + /// Download a single artwork file if it doesn't already exist + Future _downloadSingleArtwork( + String serverId, + String artworkPath, + PlexClient client, + ) async { + try { + // Check if already downloaded (deduplication) + if (await _storageService.artworkExists(serverId, artworkPath)) { + appLogger.d('Artwork already exists: $artworkPath'); + return; + } + + final url = client.getThumbnailUrl(artworkPath); + if (url.isEmpty) { + appLogger.w('Empty thumbnail URL for: $artworkPath'); + return; + } + + final filePath = await _storageService.getArtworkPathFromThumb( + serverId, + artworkPath, + ); + final file = File(filePath); + + // Ensure parent directory exists + await file.parent.create(recursive: true); + + // Download the artwork + await _dio.download(url, filePath); + appLogger.i('Downloaded artwork: $artworkPath -> $filePath'); + } catch (e, stack) { + appLogger.w( + 'Failed to download artwork: $artworkPath', + error: e, + stackTrace: stack, + ); + // Don't throw - artwork download failures shouldn't kill the entire download + } + } + + /// Download all artwork for a metadata item (public method for parent metadata) + /// Downloads thumb/poster, clearLogo, and background art + Future downloadArtworkForMetadata( + PlexMetadata metadata, + PlexClient client, + ) async { + if (metadata.serverId == null) return; + final serverId = metadata.serverId!; + + // Download thumb/poster + if (metadata.thumb != null) { + await _downloadSingleArtwork(serverId, metadata.thumb!, client); + } + + // Download clear logo + if (metadata.clearLogo != null) { + await _downloadSingleArtwork(serverId, metadata.clearLogo!, client); + } + + // Download background art + if (metadata.art != null) { + await _downloadSingleArtwork(serverId, metadata.art!, client); + } + } + + /// Download chapter thumbnail images for a media item + Future _downloadChapterThumbnails( + String serverId, + String ratingKey, + PlexClient client, + ) async { + try { + // Get chapters from the cached API response + final extras = await client.getPlaybackExtras(ratingKey); + + for (final chapter in extras.chapters) { + if (chapter.thumb != null) { + await _downloadSingleArtwork(serverId, chapter.thumb!, client); + } + } + + if (extras.chapters.isNotEmpty) { + appLogger.d('Downloaded ${extras.chapters.length} chapter thumbnails'); + } + } catch (e) { + appLogger.w('Failed to download chapter thumbnails', error: e); + // Don't fail the entire download if chapter thumbnails fail + } + } + + /// [showYear]: For episodes, pass the show's premiere year (not the episode's year) + Future _downloadSubtitles( + String globalKey, + PlexMetadata metadata, + PlexMediaInfo mediaInfo, + PlexClient client, { + int? showYear, + }) async { + try { + _emitProgress( + globalKey, + DownloadStatus.downloading, + 0, + currentFile: 'subtitles', + ); + + for (final subtitle in mediaInfo.subtitleTracks) { + // Only download external subtitles + if (!subtitle.isExternal || subtitle.key == null) { + continue; + } + + final baseUrl = client.config.baseUrl; + final token = client.config.token ?? ''; + final subtitleUrl = subtitle.getSubtitleUrl(baseUrl, token); + if (subtitleUrl == null) continue; + + // Determine file extension + final extension = _getExtensionFromCodec(subtitle.codec); + + // Get user-friendly subtitle path based on media type + final String subtitlePath; + if (metadata.type == 'episode') { + subtitlePath = await _storageService.getEpisodeSubtitlePath( + metadata, + subtitle.id, + extension, + showYear: showYear, + ); + } else if (metadata.type == 'movie') { + subtitlePath = await _storageService.getMovieSubtitlePath( + metadata, + subtitle.id, + extension, + ); + } else { + // Fallback to old structure + subtitlePath = await _storageService.getSubtitlePath( + metadata.serverId!, + metadata.ratingKey, + subtitle.id, + extension, + ); + } + + // Download subtitle file + final file = File(subtitlePath); + await file.parent.create(recursive: true); + await _dio.download(subtitleUrl, subtitlePath); + + appLogger.d('Downloaded subtitle ${subtitle.id} for $globalKey'); + } + } catch (e) { + appLogger.w('Failed to download subtitles for $globalKey', error: e); + // Don't fail the entire download if subtitles fail + } + } + + String? _getExtensionFromUrl(String url) { + final uri = Uri.tryParse(url); + if (uri == null) return null; + final path = uri.path; + final lastDot = path.lastIndexOf('.'); + if (lastDot == -1) return null; + return path.substring(lastDot + 1).split('?').first; + } + + String _getExtensionFromCodec(String? codec) { + if (codec == null) return 'srt'; + + switch (codec.toLowerCase()) { + case 'subrip': + case 'srt': + return 'srt'; + case 'ass': + return 'ass'; + case 'ssa': + return 'ssa'; + case 'webvtt': + case 'vtt': + return 'vtt'; + case 'mov_text': + return 'srt'; + case 'pgs': + case 'hdmv_pgs_subtitle': + return 'sup'; + case 'dvd_subtitle': + case 'dvdsub': + return 'sub'; + default: + return 'srt'; + } + } + + void _emitProgress( + String globalKey, + DownloadStatus status, + int progress, { + String? errorMessage, + String? currentFile, + }) { + _progressController.add( + DownloadProgress( + globalKey: globalKey, + status: status, + progress: progress, + errorMessage: errorMessage, + currentFile: currentFile, + ), + ); + } + + /// Emit progress update with artwork paths so DownloadProvider can sync + void _emitProgressWithArtwork(String globalKey, {String? thumbPath}) { + // Emit a progress update containing artwork path + // The status is preserved as downloading since artwork is just one step + _progressController.add( + DownloadProgress( + globalKey: globalKey, + status: DownloadStatus.downloading, + progress: 0, + currentFile: 'artwork', + thumbPath: thumbPath, + ), + ); + } + + /// Pause a download (works for both downloading and queued items) + Future pauseDownload(String globalKey) async { + // Cancel active download if exists + final cancelToken = _activeDownloads[globalKey]; + if (cancelToken != null) { + cancelToken.cancel('Paused by user'); + _activeDownloads.remove(globalKey); + } + // Update status to paused + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.paused.index, + ); + // Remove from queue so it doesn't restart + await _database.removeFromQueue(globalKey); + _emitProgress(globalKey, DownloadStatus.paused, 0); + } + + /// Resume a paused download + Future resumeDownload(String globalKey, PlexClient client) async { + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.queued.index, + ); + // Re-add to queue (pauseDownload removes from queue) + await _database.addToQueue(mediaGlobalKey: globalKey); + _emitProgress(globalKey, DownloadStatus.queued, 0); + _processQueue(client); + } + + /// Retry a failed download + Future retryDownload(String globalKey, PlexClient client) async { + // Clear error and reset retry count + await _database.clearDownloadError(globalKey); + // Reset status to queued + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.queued.index, + ); + // Re-add to queue + await _database.addToQueue(mediaGlobalKey: globalKey); + _emitProgress(globalKey, DownloadStatus.queued, 0); + _processQueue(client); + } + + /// Cancel a download + Future cancelDownload(String globalKey) async { + final cancelToken = _activeDownloads[globalKey]; + if (cancelToken != null) { + cancelToken.cancel('Cancelled by user'); + _activeDownloads.remove(globalKey); + } + await _database.updateDownloadStatus( + globalKey, + DownloadStatus.cancelled.index, + ); + await _database.removeFromQueue(globalKey); + _emitProgress(globalKey, DownloadStatus.cancelled, 0); + } + + /// Delete a downloaded item and its files + Future deleteDownload(String globalKey) async { + // Cancel if actively downloading + final cancelToken = _activeDownloads[globalKey]; + if (cancelToken != null) { + cancelToken.cancel('Download deleted'); + _activeDownloads.remove(globalKey); + } + + // Delete files from storage + final parts = globalKey.split(':'); + if (parts.length != 2) { + await _database.deleteDownload(globalKey); + return; + } + + final serverId = parts[0]; + final ratingKey = parts[1]; + final metadata = await _getMetadataFromCache(serverId, ratingKey); + + if (metadata == null) { + // Fallback deletion without progress + await _deleteMediaFilesWithMetadata(serverId, ratingKey); + await _apiCache.deleteForItem(serverId, ratingKey); + await _database.deleteDownload(globalKey); + return; + } + + // Determine total items to delete + final totalItems = await _getTotalItemsToDelete(metadata, serverId); + + // Emit initial progress + _emitDeletionProgress( + DeletionProgress( + globalKey: globalKey, + itemTitle: metadata.title, + currentItem: 0, + totalItems: totalItems, + ), + ); + + // Delete files from storage (with progress updates) + await _deleteMediaFilesWithMetadata(serverId, ratingKey); + + // Delete from API cache + await _apiCache.deleteForItem(serverId, ratingKey); + + // Delete from database + await _database.deleteDownload(globalKey); + + // Emit completion + _emitDeletionProgress( + DeletionProgress( + globalKey: globalKey, + itemTitle: metadata.title, + currentItem: totalItems, + totalItems: totalItems, + ), + ); + } + + /// Emit deletion progress update + void _emitDeletionProgress(DeletionProgress progress) { + _deletionProgressController.add(progress); + } + + /// Calculate total items to delete (for progress tracking) + Future _getTotalItemsToDelete( + PlexMetadata metadata, + String serverId, + ) async { + switch (metadata.type.toLowerCase()) { + case 'episode': + return 1; // Single episode + case 'movie': + return 1; // Single movie + case 'season': + // Count episodes in season + final seasonKey = metadata.ratingKey; + final episodes = await (_database.select( + _database.downloadedMedia, + )..where((t) => t.parentRatingKey.equals(seasonKey))).get(); + return episodes.length; + case 'show': + // Count all episodes in show + final showKey = metadata.ratingKey; + final episodes = await (_database.select( + _database.downloadedMedia, + )..where((t) => t.grandparentRatingKey.equals(showKey))).get(); + return episodes.length; + default: + return 1; + } + } + + /// Delete media files using metadata to find correct paths + Future _deleteMediaFilesWithMetadata( + String serverId, + String ratingKey, + ) async { + try { + // Get metadata from API cache + final metadata = await _getMetadataFromCache(serverId, ratingKey); + + if (metadata == null) { + // Fallback: Try database record + final downloadRecord = await _database.getDownloadedMedia( + '$serverId:$ratingKey', + ); + if (downloadRecord?.videoFilePath != null) { + await _deleteByFilePath(downloadRecord!); + return; + } + appLogger.w('Cannot delete - no metadata for $serverId:$ratingKey'); + return; + } + + // Delete based on type + switch (metadata.type.toLowerCase()) { + case 'episode': + await _deleteEpisodeFiles(metadata, serverId); + break; + case 'season': + await _deleteSeasonFiles(metadata, serverId); + break; + case 'show': + await _deleteShowFiles(metadata, serverId); + break; + case 'movie': + await _deleteMovieFiles(metadata, serverId); + break; + default: + appLogger.w('Unknown type for deletion: ${metadata.type}'); + } + } catch (e, stack) { + appLogger.e('Error deleting files', error: e, stackTrace: stack); + } + } + + /// Get metadata from API cache + Future _getMetadataFromCache( + String serverId, + String ratingKey, + ) async { + final cachedData = await _apiCache.get( + serverId, + '/library/metadata/$ratingKey', + ); + if (cachedData != null && cachedData['MediaContainer'] != null) { + final container = cachedData['MediaContainer'] as Map; + if (container['Metadata'] != null && + (container['Metadata'] as List).isNotEmpty) { + final metadataJson = container['Metadata'][0] as Map; + return PlexMetadata.fromJson(metadataJson).copyWith(serverId: serverId); + } + } + return null; + } + + /// Get chapter thumb paths from cached metadata + Future> _getChapterThumbPaths( + String serverId, + String ratingKey, + ) async { + try { + final cachedData = await _apiCache.get( + serverId, + '/library/metadata/$ratingKey', + ); + + if (cachedData == null || cachedData['MediaContainer'] == null) { + return []; + } + + final container = cachedData['MediaContainer'] as Map; + final metadataList = container['Metadata'] as List?; + + if (metadataList == null || metadataList.isEmpty) { + return []; + } + + final metadata = metadataList[0] as Map; + final chapters = metadata['Chapter'] as List?; + + if (chapters == null) { + return []; + } + + final thumbPaths = []; + for (final chapter in chapters) { + final thumbPath = chapter['thumb'] as String?; + if (thumbPath != null && thumbPath.isNotEmpty) { + thumbPaths.add(thumbPath); + } + } + + return thumbPaths; + } catch (e) { + appLogger.w('Error getting chapter thumb paths for $ratingKey', error: e); + return []; + } + } + + /// Check if a chapter thumbnail is used by any other downloaded items + Future _isChapterThumbnailInUse( + String serverId, + String thumbPath, + String excludeRatingKey, + ) async { + try { + // Get all downloaded items + final allItems = await _database.select(_database.downloadedMedia).get(); + + // Check if any other item uses this chapter thumbnail + for (final item in allItems) { + // Skip the item being deleted + if (item.ratingKey == excludeRatingKey) { + continue; + } + + // Get chapter thumb paths for this item + final itemChapterPaths = await _getChapterThumbPaths( + serverId, + item.ratingKey, + ); + + // Check if this item has the same thumb path + if (itemChapterPaths.contains(thumbPath)) { + return true; // Thumbnail is in use + } + } + + return false; // Thumbnail is not in use + } catch (e) { + appLogger.w( + 'Error checking chapter thumbnail usage: $thumbPath', + error: e, + ); + // On error, assume in use to be safe (don't delete) + return true; + } + } + + /// Delete chapter thumbnails for a media item (with reference counting) + Future _deleteChapterThumbnails( + String serverId, + String ratingKey, + ) async { + try { + final thumbPaths = await _getChapterThumbPaths(serverId, ratingKey); + + if (thumbPaths.isEmpty) { + appLogger.d('No chapter thumbnails to delete for $ratingKey'); + return; + } + + int deletedCount = 0; + int preservedCount = 0; + + for (final thumbPath in thumbPaths) { + try { + // Check if this thumbnail is used by other items + final inUse = await _isChapterThumbnailInUse( + serverId, + thumbPath, + ratingKey, + ); + + if (inUse) { + appLogger.d('Preserving chapter thumbnail (in use): $thumbPath'); + preservedCount++; + continue; + } + + // Get artwork file path and delete + final artworkPath = await _storageService.getArtworkPathFromThumb( + serverId, + thumbPath, + ); + final file = File(artworkPath); + + if (await file.exists()) { + await file.delete(); + deletedCount++; + appLogger.d('Deleted chapter thumbnail: $thumbPath'); + } + } catch (e) { + appLogger.w( + 'Failed to delete chapter thumbnail: $thumbPath', + error: e, + ); + // Continue with other chapters even if one fails + } + } + + if (deletedCount > 0 || preservedCount > 0) { + appLogger.i( + 'Deleted $deletedCount of ${thumbPaths.length} chapter thumbnails ($preservedCount preserved)', + ); + } + } catch (e, stack) { + appLogger.w( + 'Error deleting chapter thumbnails for $ratingKey', + error: e, + stackTrace: stack, + ); + // Don't throw - chapter deletion shouldn't block main deletion + } + } + + /// Delete episode files + Future _deleteEpisodeFiles( + PlexMetadata episode, + String serverId, + ) async { + try { + final parentMetadata = episode.grandparentRatingKey != null + ? await _getMetadataFromCache(serverId, episode.grandparentRatingKey!) + : null; + final showYear = parentMetadata?.year; + + // Delete video file + final videoPathTemplate = await _storageService.getEpisodeVideoPath( + episode, + 'tmp', + showYear: showYear, + ); + final videoPathWithoutExt = videoPathTemplate.substring( + 0, + videoPathTemplate.lastIndexOf('.'), + ); + final actualVideoFile = await _findFileWithAnyExtension( + videoPathWithoutExt, + ); + if (actualVideoFile != null && await actualVideoFile.exists()) { + await actualVideoFile.delete(); + appLogger.i('Deleted episode video: ${actualVideoFile.path}'); + } + + // Delete thumbnail + final thumbPath = await _storageService.getEpisodeThumbnailPath( + episode, + showYear: showYear, + ); + final thumbFile = File(thumbPath); + if (await thumbFile.exists()) { + await thumbFile.delete(); + appLogger.i('Deleted episode thumbnail: $thumbPath'); + } + + // Delete subtitles directory + final subsDir = await _storageService.getEpisodeSubtitlesDirectory( + episode, + showYear: showYear, + ); + if (await subsDir.exists()) { + await subsDir.delete(recursive: true); + appLogger.i('Deleted episode subtitles: ${subsDir.path}'); + } + + // Delete chapter thumbnails (with reference counting) + await _deleteChapterThumbnails(serverId, episode.ratingKey); + + // Clean up parent directories if empty + await _cleanupEmptyDirectories(episode, showYear); + } catch (e, stack) { + appLogger.e('Error deleting episode files', error: e, stackTrace: stack); + } + } + + /// Delete season files + Future _deleteSeasonFiles(PlexMetadata season, String serverId) async { + try { + final parentMetadata = season.parentRatingKey != null + ? await _getMetadataFromCache(serverId, season.parentRatingKey!) + : null; + final showYear = parentMetadata?.year; + + // Get all episodes in this season + final seasonKey = season.ratingKey; + final episodesInSeason = await (_database.select( + _database.downloadedMedia, + )..where((t) => t.parentRatingKey.equals(seasonKey))).get(); + + appLogger.d( + 'Deleting ${episodesInSeason.length} episodes in season $seasonKey', + ); + for (int i = 0; i < episodesInSeason.length; i++) { + final episode = episodesInSeason[i]; + final episodeGlobalKey = '$serverId:${episode.ratingKey}'; + + // Emit progress update + _emitDeletionProgress( + DeletionProgress( + globalKey: '$serverId:$seasonKey', + itemTitle: season.title, + currentItem: i + 1, + totalItems: episodesInSeason.length, + currentOperation: + 'Deleting episode ${i + 1} of ${episodesInSeason.length}', + ), + ); + + // Delete chapter thumbnails + await _deleteChapterThumbnails(serverId, episode.ratingKey); + + // Delete episode files (video, subtitles) if stored outside season directory + await _deleteByFilePath(episode); + + // Delete episode from API cache + await _apiCache.deleteForItem(serverId, episode.ratingKey); + + // Delete episode DB entry + await _database.deleteDownload(episodeGlobalKey); + } + + final seasonDir = await _storageService.getSeasonDirectory( + season, + showYear: showYear, + ); + if (await seasonDir.exists()) { + await seasonDir.delete(recursive: true); + appLogger.i('Deleted season directory: ${seasonDir.path}'); + } + + await _cleanupShowDirectory(season, showYear); + } catch (e, stack) { + appLogger.e('Error deleting season files', error: e, stackTrace: stack); + } + } + + /// Delete show files + Future _deleteShowFiles(PlexMetadata show, String serverId) async { + try { + // Get all episodes in this show + final showKey = show.ratingKey; + final episodesInShow = await (_database.select( + _database.downloadedMedia, + )..where((t) => t.grandparentRatingKey.equals(showKey))).get(); + + appLogger.d( + 'Deleting ${episodesInShow.length} episodes in show $showKey', + ); + for (int i = 0; i < episodesInShow.length; i++) { + final episode = episodesInShow[i]; + final episodeGlobalKey = '$serverId:${episode.ratingKey}'; + + // Emit progress update + _emitDeletionProgress( + DeletionProgress( + globalKey: '$serverId:$showKey', + itemTitle: show.title, + currentItem: i + 1, + totalItems: episodesInShow.length, + currentOperation: + 'Deleting episode ${i + 1} of ${episodesInShow.length}', + ), + ); + + // Delete chapter thumbnails + await _deleteChapterThumbnails(serverId, episode.ratingKey); + + // Delete episode files (video, subtitles) if stored outside show directory + await _deleteByFilePath(episode); + + // Delete episode from API cache + await _apiCache.deleteForItem(serverId, episode.ratingKey); + + // Delete episode DB entry + await _database.deleteDownload(episodeGlobalKey); + } + + final showDir = await _storageService.getShowDirectory(show); + if (await showDir.exists()) { + await showDir.delete(recursive: true); + appLogger.i('Deleted show directory: ${showDir.path}'); + } + } catch (e, stack) { + appLogger.e('Error deleting show files', error: e, stackTrace: stack); + } + } + + /// Delete movie files + Future _deleteMovieFiles(PlexMetadata movie, String serverId) async { + try { + final movieDir = await _storageService.getMovieDirectory(movie); + if (await movieDir.exists()) { + await movieDir.delete(recursive: true); + appLogger.i('Deleted movie directory: ${movieDir.path}'); + } + + // Delete chapter thumbnails (with reference counting) + await _deleteChapterThumbnails(serverId, movie.ratingKey); + } catch (e, stack) { + appLogger.e('Error deleting movie files', error: e, stackTrace: stack); + } + } + + /// Clean up empty directories after deleting episode + Future _cleanupEmptyDirectories( + PlexMetadata episode, + int? showYear, + ) async { + final seasonDir = await _storageService.getSeasonDirectory( + episode, + showYear: showYear, + ); + + if (await seasonDir.exists()) { + final contents = await seasonDir.list().toList(); + final hasVideos = contents.any( + (e) => + e.path.endsWith('.mp4') || + e.path.endsWith('.ogv') || + e.path.endsWith('.mkv') || + e.path.endsWith('.m4v') || + e.path.endsWith('.avi') || + e.path.contains('_subs'), + ); + + if (!hasVideos) { + if (!await _isSeasonArtworkInUse(episode, showYear)) { + await seasonDir.delete(recursive: true); + appLogger.i('Deleted empty season directory: ${seasonDir.path}'); + await _cleanupShowDirectory(episode, showYear); + } + } + } + } + + /// Clean up show directory if empty + Future _cleanupShowDirectory( + PlexMetadata metadata, + int? showYear, + ) async { + final showDir = await _storageService.getShowDirectory( + metadata, + showYear: showYear, + ); + + if (await showDir.exists()) { + final contents = await showDir.list().toList(); + final hasSeasons = contents.any( + (e) => e is Directory && e.path.contains('Season '), + ); + + if (!hasSeasons) { + if (!await _isShowArtworkInUse(metadata, showYear)) { + await showDir.delete(recursive: true); + appLogger.i('Deleted empty show directory: ${showDir.path}'); + } + } + } + } + + /// Check if season artwork is in use + Future _isSeasonArtworkInUse( + PlexMetadata episode, + int? showYear, + ) async { + final seasonKey = episode.parentRatingKey; + if (seasonKey == null) return false; + + final otherEpisodes = await (_database.select( + _database.downloadedMedia, + )..where((t) => t.parentRatingKey.equals(seasonKey))).get(); + + // Check if any episodes besides this one + return otherEpisodes.any( + (e) => e.globalKey != '${episode.serverId}:${episode.ratingKey}', + ); + } + + /// Check if show artwork is in use + Future _isShowArtworkInUse(PlexMetadata metadata, int? showYear) async { + final showKey = + metadata.grandparentRatingKey ?? + metadata.parentRatingKey ?? + metadata.ratingKey; + + final allItems = await _database.select(_database.downloadedMedia).get(); + + // Check if any items belong to this show besides this one + return allItems.any( + (item) => + (item.grandparentRatingKey == showKey || + item.parentRatingKey == showKey) && + item.globalKey != '${metadata.serverId}:${metadata.ratingKey}', + ); + } + + /// Find file with any extension + Future _findFileWithAnyExtension(String pathWithoutExt) async { + final dir = Directory(path.dirname(pathWithoutExt)); + final baseName = path.basename(pathWithoutExt); + + if (!await dir.exists()) return null; + + try { + final files = await dir + .list() + .where( + (e) => + e is File && path.basenameWithoutExtension(e.path) == baseName, + ) + .toList(); + + return files.isNotEmpty ? files.first as File : null; + } catch (e) { + appLogger.w('Error finding file: $pathWithoutExt', error: e); + return null; + } + } + + /// Fallback deletion using file paths from database + Future _deleteByFilePath(DownloadedMediaItem record) async { + try { + if (record.videoFilePath != null) { + final videoPath = await _storageService.toAbsolutePath( + record.videoFilePath!, + ); + final videoFile = File(videoPath); + if (await videoFile.exists()) { + await videoFile.delete(); + appLogger.i('Deleted video file: $videoPath'); + + // Delete subtitle directory + final subsPath = videoPath.replaceAll(RegExp(r'\.[^.]+$'), '_subs'); + final subsDir = Directory(subsPath); + if (await subsDir.exists()) { + await subsDir.delete(recursive: true); + appLogger.i('Deleted subtitles: $subsPath'); + } + } + } + + if (record.thumbPath != null) { + final thumbPath = await _storageService.toAbsolutePath( + record.thumbPath!, + ); + final thumbFile = File(thumbPath); + if (await thumbFile.exists()) { + await thumbFile.delete(); + appLogger.i('Deleted thumbnail: $thumbPath'); + } + } + } catch (e, stack) { + appLogger.e('Error in fallback deletion', error: e, stackTrace: stack); + } + } + + /// Get all downloads with a specific status + Stream> watchDownloadsByStatus( + DownloadStatus status, + ) { + return (_database.select( + _database.downloadedMedia, + )..where((t) => t.status.equals(status.index))).watch(); + } + + /// Get all downloaded media items (for loading persisted data) + Future> getAllDownloads() async { + return _database.select(_database.downloadedMedia).get(); + } + + /// Get a specific downloaded media item by globalKey + Future getDownloadedMedia(String globalKey) async { + return _database.getDownloadedMedia(globalKey); + } + + /// Save metadata for a media item (show, season, movie, or episode) + /// Used to persist parent metadata (shows/seasons) for offline display + Future saveMetadata(PlexMetadata metadata) async { + if (metadata.serverId == null) { + appLogger.w('Cannot save metadata without serverId'); + return; + } + + // Cache to API cache for offline use + await _cacheMetadataForOffline( + metadata.serverId!, + metadata.ratingKey, + metadata, + ); + } + + /// Cache metadata in the API response format for offline access + /// This simulates what PlexClient would receive from the server + Future _cacheMetadataForOffline( + String serverId, + String ratingKey, + PlexMetadata metadata, + ) async { + final endpoint = '/library/metadata/$ratingKey'; + + // Build a response structure that matches the Plex API format + final cachedResponse = { + 'MediaContainer': { + 'Metadata': [metadata.toJson()], + }, + }; + + await _apiCache.put(serverId, endpoint, cachedResponse); + await _apiCache.pinForOffline(serverId, ratingKey); + } + + /// Cache children (seasons or episodes) in the API response format + Future cacheChildrenForOffline( + String serverId, + String parentRatingKey, + List children, + ) async { + final endpoint = '/library/metadata/$parentRatingKey/children'; + + // Build a response structure that matches the Plex API format + final cachedResponse = { + 'MediaContainer': {'Metadata': children.map((c) => c.toJson()).toList()}, + }; + + await _apiCache.put(serverId, endpoint, cachedResponse); + } + + void dispose() { + _connectivitySubscription?.cancel(); + for (final token in _activeDownloads.values) { + token.cancel('Service disposed'); + } + _progressController.close(); + _deletionProgressController.close(); + } +} diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart new file mode 100644 index 00000000..e05449da --- /dev/null +++ b/lib/services/download_storage_service.dart @@ -0,0 +1,710 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:crypto/crypto.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; + +import '../models/plex_metadata.dart'; +import 'settings_service.dart'; +import 'saf_storage_service.dart'; + +class DownloadStorageService { + static DownloadStorageService? _instance; + static DownloadStorageService get instance => + _instance ??= DownloadStorageService._(); + DownloadStorageService._(); + + Directory? _baseDownloadsDir; + String? _artworkDirectoryPath; + + // Custom path configuration + SettingsService? _settingsService; + String? _customDownloadPath; + String _customPathType = 'file'; + + /// Check if currently using SAF mode (Android only) + bool get isUsingSaf => + Platform.isAndroid && + _customPathType == 'saf' && + _customDownloadPath != null; + + /// Get the SAF base URI (only valid when isUsingSaf is true) + String? get safBaseUri => isUsingSaf ? _customDownloadPath : null; + + /// Get artwork directory path (cached, synchronous after first call) + String? get artworkDirectoryPath => _artworkDirectoryPath; + + /// Initialize with settings service (call during app startup) + Future initialize(SettingsService settingsService) async { + _settingsService = settingsService; + _customDownloadPath = settingsService.getCustomDownloadPath(); + _customPathType = settingsService.getCustomDownloadPathType(); + // Reset cached directories to force recalculation + _baseDownloadsDir = null; + _artworkDirectoryPath = null; + } + + /// Refresh custom path from settings (call when settings change) + Future refreshCustomPath() async { + if (_settingsService != null) { + _customDownloadPath = _settingsService!.getCustomDownloadPath(); + _customPathType = _settingsService!.getCustomDownloadPathType(); + _baseDownloadsDir = null; + _artworkDirectoryPath = null; + } + } + + /// Check if using custom download path + bool isUsingCustomPath() => _customDownloadPath != null; + + /// Get current download path for display in settings + Future getCurrentDownloadPathDisplay() async { + if (_customDownloadPath != null) { + return _customDownloadPath!; + } + final dir = await getDownloadsDirectory(); + return dir.path; + } + + /// Get default download path (for "Reset to Default" functionality) + Future getDefaultDownloadPath() async { + final Directory baseDir; + if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationDocumentsDirectory(); + } else { + baseDir = await getApplicationSupportDirectory(); + } + return path.join(baseDir.path, 'downloads'); + } + + /// Check if a directory is writable + Future isDirectoryWritable(Directory dir) async { + try { + if (!await dir.exists()) { + await dir.create(recursive: true); + } + // Test write access with a temp file + final testFile = File( + path.join( + dir.path, + '.write_test_${DateTime.now().millisecondsSinceEpoch}', + ), + ); + await testFile.writeAsString('test'); + await testFile.delete(); + return true; + } catch (e) { + return false; + } + } + + /// Initialize and get base downloads directory + Future getDownloadsDirectory() async { + if (_baseDownloadsDir != null) return _baseDownloadsDir!; + + // Check for custom path first (file type only - SAF handled differently) + if (_customDownloadPath != null && _customPathType == 'file') { + final customDir = Directory(_customDownloadPath!); + if (await isDirectoryWritable(customDir)) { + _baseDownloadsDir = customDir; + return _baseDownloadsDir!; + } + // Fall through to default if custom path is not writable + } + + // Default path logic + final Directory baseDir; + if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationDocumentsDirectory(); + } else { + // Desktop: macOS, Windows, Linux + baseDir = await getApplicationSupportDirectory(); + } + + _baseDownloadsDir = Directory(path.join(baseDir.path, 'downloads')); + if (!await _baseDownloadsDir!.exists()) { + await _baseDownloadsDir!.create(recursive: true); + } + return _baseDownloadsDir!; + } + + /// Get centralized artwork directory for offline artwork caching + /// This directory stores artwork files with hashed filenames for deduplication + Future getArtworkDirectory() async { + // If custom download path is set, put artwork alongside downloads + if (_customDownloadPath != null && _customPathType == 'file') { + final customDir = Directory(_customDownloadPath!); + final parent = customDir.parent; + final artworkDir = Directory(path.join(parent.path, 'artwork')); + try { + if (!await artworkDir.exists()) { + await artworkDir.create(recursive: true); + } + _artworkDirectoryPath = artworkDir.path; + return artworkDir; + } catch (e) { + // Fall through to default if we can't create artwork dir + } + } + + // Default: Get the app base directory directly (not downloads directory) + final Directory baseDir; + if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationDocumentsDirectory(); + } else { + // Desktop: macOS, Windows, Linux + baseDir = await getApplicationSupportDirectory(); + } + + final artworkDir = Directory(path.join(baseDir.path, 'artwork')); + if (!await artworkDir.exists()) { + await artworkDir.create(recursive: true); + } + // Cache the path for synchronous access + _artworkDirectoryPath = artworkDir.path; + return artworkDir; + } + + /// Get artwork file path from Plex thumb path (synchronous, requires initialization) + /// Returns path to cached artwork file using hash of the thumb URL + /// Example: artwork/a1b2c3d4e5f6.jpg + String getArtworkPathSync(String serverId, String thumbPath) { + if (_artworkDirectoryPath == null) { + throw StateError( + 'Artwork directory not initialized. Call getArtworkDirectory() first.', + ); + } + // Create hash from serverId:thumbPath for deduplication + final hash = _hashArtworkPath(serverId, thumbPath); + return path.join(_artworkDirectoryPath!, '$hash.jpg'); + } + + /// Get artwork file path from Plex thumb path (async version) + Future getArtworkPathFromThumb( + String serverId, + String thumbPath, + ) async { + final artworkDir = await getArtworkDirectory(); + final hash = _hashArtworkPath(serverId, thumbPath); + return path.join(artworkDir.path, '$hash.jpg'); + } + + /// Check if artwork already exists (for deduplication) + Future artworkExists(String serverId, String thumbPath) async { + final artworkPath = await getArtworkPathFromThumb(serverId, thumbPath); + return File(artworkPath).exists(); + } + + /// Hash artwork path for filename using MD5 for stability across app restarts + String _hashArtworkPath(String serverId, String thumbPath) { + final combined = '$serverId:$thumbPath'; + return md5.convert(utf8.encode(combined)).toString(); + } + + /// Get directory for a specific media item + Future getMediaDirectory(String serverId, String ratingKey) async { + final baseDir = await getDownloadsDirectory(); + final mediaDir = Directory(path.join(baseDir.path, serverId, ratingKey)); + if (!await mediaDir.exists()) { + await mediaDir.create(recursive: true); + } + return mediaDir; + } + + /// Get video file path + Future getVideoFilePath( + String serverId, + String ratingKey, + String extension, + ) async { + final mediaDir = await getMediaDirectory(serverId, ratingKey); + return path.join(mediaDir.path, 'video.$extension'); + } + + /// Get artwork file path (poster, art, thumb) + Future getArtworkPath( + String serverId, + String ratingKey, + String artworkType, + ) async { + final mediaDir = await getMediaDirectory(serverId, ratingKey); + return path.join(mediaDir.path, '$artworkType.jpg'); + } + + /// Get subtitles directory + Future getSubtitlesDirectory( + String serverId, + String ratingKey, + ) async { + final mediaDir = await getMediaDirectory(serverId, ratingKey); + final subtitlesDir = Directory(path.join(mediaDir.path, 'subtitles')); + if (!await subtitlesDir.exists()) { + await subtitlesDir.create(recursive: true); + } + return subtitlesDir; + } + + /// Get subtitle file path + Future getSubtitlePath( + String serverId, + String ratingKey, + int trackId, + String extension, + ) async { + final subtitlesDir = await getSubtitlesDirectory(serverId, ratingKey); + return path.join(subtitlesDir.path, '$trackId.$extension'); + } + + // ============================================================ + // USER-FRIENDLY PATH METHODS (for Files app visibility) + // ============================================================ + + /// Sanitize a filename by removing invalid filesystem characters + String _sanitizeFileName(String name) { + // Remove invalid filesystem characters: < > : " / \ | ? * + // Also remove leading/trailing whitespace and dots + return name + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '') + .replaceAll(RegExp(r'^\.+|\.+$'), '') + .trim(); + } + + /// Get movie directory: downloads/Movies/{Movie Name} ({Year})/ + Future getMovieDirectory(PlexMetadata movie) async { + final baseDir = await getDownloadsDirectory(); + final movieName = _sanitizeFileName(movie.title); + final year = movie.year; + final movieFolder = year != null ? '$movieName ($year)' : movieName; + final dir = Directory(path.join(baseDir.path, 'Movies', movieFolder)); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + /// Get movie video file path: .../Movie Name (YYYY)/Movie Name (YYYY).{ext} + Future getMovieVideoPath(PlexMetadata movie, String extension) async { + final movieDir = await getMovieDirectory(movie); + final movieName = _sanitizeFileName(movie.title); + final year = movie.year; + final fileName = year != null ? '$movieName ($year)' : movieName; + return path.join(movieDir.path, '$fileName.$extension'); + } + + /// Get movie artwork path: .../Movie Name (YYYY)/{artworkType}.jpg + Future getMovieArtworkPath( + PlexMetadata movie, + String artworkType, + ) async { + final movieDir = await getMovieDirectory(movie); + return path.join(movieDir.path, '$artworkType.jpg'); + } + + /// Get show directory: downloads/TV Shows/{Show Name} ({Year})/ + /// [showYear]: Pass the show's premiere year explicitly (for episodes, the episode's + /// year may differ from the show's year). If not provided, uses metadata.year. + Future getShowDirectory( + PlexMetadata metadata, { + int? showYear, + }) async { + final baseDir = await getDownloadsDirectory(); + // For episodes, use grandparentTitle; for shows, use title + final showName = _sanitizeFileName( + metadata.grandparentTitle ?? metadata.title, + ); + // Use explicit showYear if provided, otherwise fall back to metadata.year + final year = showYear ?? metadata.year; + final showFolder = year != null ? '$showName ($year)' : showName; + final dir = Directory(path.join(baseDir.path, 'TV Shows', showFolder)); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + /// Get show artwork path: downloads/TV Shows/{Show}/poster.jpg + Future getShowArtworkPath( + PlexMetadata metadata, + String artworkType, { + int? showYear, + }) async { + final showDir = await getShowDirectory(metadata, showYear: showYear); + return path.join(showDir.path, '$artworkType.jpg'); + } + + /// Get season directory: .../TV Shows/{Show}/Season {XX}/ + /// [showYear]: Pass the show's premiere year (not episode or season year) + Future getSeasonDirectory( + PlexMetadata metadata, { + int? showYear, + }) async { + final showDir = await getShowDirectory(metadata, showYear: showYear); + final seasonNum = (metadata.parentIndex ?? 0).toString().padLeft(2, '0'); + final dir = Directory(path.join(showDir.path, 'Season $seasonNum')); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + /// Get season artwork path: .../Season XX/poster.jpg + Future getSeasonArtworkPath( + PlexMetadata metadata, + String artworkType, { + int? showYear, + }) async { + final seasonDir = await getSeasonDirectory(metadata, showYear: showYear); + return path.join(seasonDir.path, '$artworkType.jpg'); + } + + /// Get episode video file path: .../Season XX/S{XX}E{XX} - {Title}.{ext} + /// [showYear]: Pass the show's premiere year (not episode year) + Future getEpisodeVideoPath( + PlexMetadata episode, + String extension, { + int? showYear, + }) async { + final seasonDir = await getSeasonDirectory(episode, showYear: showYear); + final seasonNum = (episode.parentIndex ?? 0).toString().padLeft(2, '0'); + final episodeNum = (episode.index ?? 0).toString().padLeft(2, '0'); + final episodeName = _sanitizeFileName(episode.title); + return path.join( + seasonDir.path, + 'S${seasonNum}E$episodeNum - $episodeName.$extension', + ); + } + + /// Get episode thumbnail path: .../Season XX/S{XX}E{XX} - {Title}.jpg + /// [showYear]: Pass the show's premiere year (not episode year) + Future getEpisodeThumbnailPath( + PlexMetadata episode, { + int? showYear, + }) async { + final seasonDir = await getSeasonDirectory(episode, showYear: showYear); + final seasonNum = (episode.parentIndex ?? 0).toString().padLeft(2, '0'); + final episodeNum = (episode.index ?? 0).toString().padLeft(2, '0'); + final episodeName = _sanitizeFileName(episode.title); + return path.join( + seasonDir.path, + 'S${seasonNum}E$episodeNum - $episodeName.jpg', + ); + } + + /// Get subtitles directory for episode: .../Season XX/S{XX}E{XX} - {Title}_subs/ + /// [showYear]: Pass the show's premiere year (not episode year) + Future getEpisodeSubtitlesDirectory( + PlexMetadata episode, { + int? showYear, + }) async { + final seasonDir = await getSeasonDirectory(episode, showYear: showYear); + final seasonNum = (episode.parentIndex ?? 0).toString().padLeft(2, '0'); + final episodeNum = (episode.index ?? 0).toString().padLeft(2, '0'); + final episodeName = _sanitizeFileName(episode.title); + final subsDir = Directory( + path.join( + seasonDir.path, + 'S${seasonNum}E$episodeNum - ${episodeName}_subs', + ), + ); + if (!await subsDir.exists()) { + await subsDir.create(recursive: true); + } + return subsDir; + } + + /// Get episode subtitle path + /// [showYear]: Pass the show's premiere year (not episode year) + Future getEpisodeSubtitlePath( + PlexMetadata episode, + int trackId, + String extension, { + int? showYear, + }) async { + final subsDir = await getEpisodeSubtitlesDirectory( + episode, + showYear: showYear, + ); + return path.join(subsDir.path, '$trackId.$extension'); + } + + /// Get subtitles directory for movie + Future getMovieSubtitlesDirectory(PlexMetadata movie) async { + final movieDir = await getMovieDirectory(movie); + final movieName = _sanitizeFileName(movie.title); + final year = movie.year; + final baseName = year != null ? '$movieName ($year)' : movieName; + final subsDir = Directory(path.join(movieDir.path, '${baseName}_subs')); + if (!await subsDir.exists()) { + await subsDir.create(recursive: true); + } + return subsDir; + } + + /// Get movie subtitle path + Future getMovieSubtitlePath( + PlexMetadata movie, + int trackId, + String extension, + ) async { + final subsDir = await getMovieSubtitlesDirectory(movie); + return path.join(subsDir.path, '$trackId.$extension'); + } + + /// Delete all files for a media item + Future deleteMediaFiles(String serverId, String ratingKey) async { + final mediaDir = await getMediaDirectory(serverId, ratingKey); + if (await mediaDir.exists()) { + await mediaDir.delete(recursive: true); + } + } + + /// Convert an absolute file path to a relative path (for database storage) + /// This ensures paths remain valid across app reinstalls on iOS where + /// the container UUID can change. + /// Returns a path relative to the app's documents directory. + Future toRelativePath(String absolutePath) async { + final Directory baseDir; + if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationDocumentsDirectory(); + } else { + baseDir = await getApplicationSupportDirectory(); + } + + // If the path starts with the base directory, strip it + if (absolutePath.startsWith(baseDir.path)) { + // Remove the base path and any leading separator + var relative = absolutePath.substring(baseDir.path.length); + if (relative.startsWith('/') || relative.startsWith('\\')) { + relative = relative.substring(1); + } + return relative; + } + + // Already relative or from a different base - return as-is + return absolutePath; + } + + /// Convert a relative file path to an absolute path (for file operations) + /// Reconstructs the full path using the current app documents directory. + Future toAbsolutePath(String relativePath) async { + // If it's already an absolute path, return as-is + if (path.isAbsolute(relativePath)) { + return relativePath; + } + + final Directory baseDir; + if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationDocumentsDirectory(); + } else { + baseDir = await getApplicationSupportDirectory(); + } + + return path.join(baseDir.path, relativePath); + } + + /// Convert a potentially absolute path (from old database entries) to absolute + /// This handles both old absolute paths and new relative paths + Future ensureAbsolutePath(String storedPath) async { + if (path.isAbsolute(storedPath)) { + // Already absolute - check if file exists at this path + if (await File(storedPath).exists()) { + return storedPath; + } + // File doesn't exist at absolute path - try to reconstruct + // Extract the relative portion (everything after 'downloads/') + final downloadsIndex = storedPath.indexOf('downloads/'); + if (downloadsIndex != -1) { + final relativePart = storedPath.substring(downloadsIndex); + return await toAbsolutePath(relativePart); + } + // Can't reconstruct, return original + return storedPath; + } + // Relative path - convert to absolute + return await toAbsolutePath(storedPath); + } + + /// Calculate total storage used by downloads + Future getTotalStorageUsed() async { + final baseDir = await getDownloadsDirectory(); + return _calculateDirectorySize(baseDir); + } + + Future _calculateDirectorySize(Directory dir) async { + int size = 0; + if (!await dir.exists()) return size; + + await for (var entity in dir.list(recursive: true, followLinks: false)) { + if (entity is File) { + try { + size += await entity.length(); + } catch (_) { + // Ignore errors reading file size + } + } + } + return size; + } + + /// Format bytes to human readable string + static String formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + if (bytes < 1024 * 1024 * 1024) { + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; + } + + // ============================================================ + // SAF (Storage Access Framework) SUPPORT FOR ANDROID + // ============================================================ + + /// Get temporary cache directory for initial downloads + /// Files are downloaded here first, then copied to SAF if using SAF mode + Future getCacheDownloadDirectory() async { + final cacheDir = await getApplicationDocumentsDirectory(); + final downloadCache = Directory( + path.join(cacheDir.path, '.download_cache'), + ); + if (!await downloadCache.exists()) { + await downloadCache.create(recursive: true); + } + return downloadCache; + } + + /// Get temporary file path for downloading (before copying to SAF) + Future getTempDownloadPath(String fileName) async { + final cacheDir = await getCacheDownloadDirectory(); + return path.join(cacheDir.path, fileName); + } + + /// Copy a file from temp cache to SAF and return the SAF URI + /// Returns null if SAF is not available or copy fails + Future copyToSaf( + String tempFilePath, + List pathComponents, + String fileName, + String mimeType, + ) async { + if (!isUsingSaf || _customDownloadPath == null) return null; + + final safService = SafStorageService.instance; + + // Create nested directory structure in SAF + final targetDirUri = await safService.createNestedDirectories( + _customDownloadPath!, + pathComponents, + ); + + if (targetDirUri == null) { + return null; + } + + // Copy the file to SAF + final safUri = await safService.copyFileToSaf( + tempFilePath, + targetDirUri, + fileName, + mimeType, + ); + + // Delete temp file after successful copy + if (safUri != null) { + try { + await File(tempFilePath).delete(); + } catch (_) { + // Ignore cleanup errors + } + } + + return safUri; + } + + /// Get the MIME type for a file extension + String getMimeType(String extension) { + switch (extension.toLowerCase()) { + case 'mp4': + return 'video/mp4'; + case 'mkv': + return 'video/x-matroska'; + case 'm4v': + return 'video/x-m4v'; + case 'avi': + return 'video/x-msvideo'; + case 'ogv': + return 'video/ogg'; + case 'webm': + return 'video/webm'; + case 'srt': + return 'application/x-subrip'; + case 'vtt': + return 'text/vtt'; + case 'ass': + case 'ssa': + return 'text/x-ssa'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + default: + return 'application/octet-stream'; + } + } + + /// Get path components for SAF based on media type + /// Returns list of directory names to create under the SAF base + List getMovieSafPathComponents(PlexMetadata movie) { + final movieName = _sanitizeFileName(movie.title); + final year = movie.year; + final movieFolder = year != null ? '$movieName ($year)' : movieName; + return ['Movies', movieFolder]; + } + + /// Get path components for episode SAF storage + List getEpisodeSafPathComponents( + PlexMetadata episode, { + int? showYear, + }) { + final showName = _sanitizeFileName( + episode.grandparentTitle ?? episode.title, + ); + final year = showYear ?? episode.year; + final showFolder = year != null ? '$showName ($year)' : showName; + final seasonNum = (episode.parentIndex ?? 0).toString().padLeft(2, '0'); + return ['TV Shows', showFolder, 'Season $seasonNum']; + } + + /// Get SAF file name for a movie + String getMovieSafFileName(PlexMetadata movie, String extension) { + final movieName = _sanitizeFileName(movie.title); + final year = movie.year; + final fileName = year != null ? '$movieName ($year)' : movieName; + return '$fileName.$extension'; + } + + /// Get SAF file name for an episode + String getEpisodeSafFileName(PlexMetadata episode, String extension) { + final seasonNum = (episode.parentIndex ?? 0).toString().padLeft(2, '0'); + final episodeNum = (episode.index ?? 0).toString().padLeft(2, '0'); + final episodeName = _sanitizeFileName(episode.title); + return 'S${seasonNum}E$episodeNum - $episodeName.$extension'; + } + + /// Check if a path is a SAF content URI + bool isSafUri(String storedPath) { + return storedPath.startsWith('content://'); + } + + /// Get a readable path for a stored path (handles both SAF URIs and file paths) + /// For SAF URIs, returns the URI as-is (content:// URIs work with media players) + /// For file paths, ensures the path is absolute + Future getReadablePath(String storedPath) async { + if (isSafUri(storedPath)) { + // SAF content:// URIs are already readable by media players + return storedPath; + } + return await ensureAbsolutePath(storedPath); + } +} diff --git a/lib/services/offline_watch_sync_service.dart b/lib/services/offline_watch_sync_service.dart new file mode 100644 index 00000000..59ad9dad --- /dev/null +++ b/lib/services/offline_watch_sync_service.dart @@ -0,0 +1,507 @@ +import 'package:flutter/foundation.dart'; + +import '../database/app_database.dart'; +import '../providers/offline_mode_provider.dart'; +import '../utils/app_logger.dart'; +import 'multi_server_manager.dart'; +import 'plex_api_cache.dart'; +import 'plex_client.dart'; + +/// Service for managing offline watch progress and syncing to Plex servers. +/// +/// Handles: +/// - Queuing progress updates when offline +/// - Queuing manual watch/unwatch actions +/// - Auto-marking items as watched at 90% threshold +/// - Syncing queued actions when connectivity is restored +class OfflineWatchSyncService extends ChangeNotifier { + final AppDatabase _database; + final MultiServerManager _serverManager; + + OfflineModeProvider? _offlineModeProvider; + VoidCallback? _offlineModeListener; + bool _isSyncing = false; + DateTime? _lastSyncTime; + bool _hasPerformedStartupSync = false; + + /// Callback to refresh download provider metadata after sync + VoidCallback? onWatchStatesRefreshed; + + /// Watch threshold - mark as watched when progress exceeds this percentage + static const double watchedThreshold = 0.90; + + /// Minimum interval between syncs (10 minutes) + static const Duration minSyncInterval = Duration(minutes: 10); + + /// Maximum sync attempts before giving up on an item + static const int maxSyncAttempts = 5; + + OfflineWatchSyncService({ + required AppDatabase database, + required MultiServerManager serverManager, + }) : _database = database, + _serverManager = serverManager; + + /// Whether a sync is currently in progress + bool get isSyncing => _isSyncing; + + /// Start monitoring for connectivity changes to auto-sync + void startConnectivityMonitoring(OfflineModeProvider offlineModeProvider) { + // Remove previous listener if any + if (_offlineModeProvider != null && _offlineModeListener != null) { + _offlineModeProvider!.removeListener(_offlineModeListener!); + } + + _offlineModeProvider = offlineModeProvider; + _offlineModeListener = () { + if (!offlineModeProvider.isOffline) { + // We just came online - trigger bidirectional sync + appLogger.i( + 'Connectivity restored - starting bidirectional watch sync', + ); + _performBidirectionalSync(); + } + }; + + offlineModeProvider.addListener(_offlineModeListener!); + + // Don't sync on startup - servers aren't connected yet. + // Sync will happen when: + // - Connectivity is restored (listener triggers) + // - App resumes from background (onAppResumed) + } + + /// Perform bidirectional sync: push local changes, then pull server states. + /// + /// Push always happens immediately. Pull respects [minSyncInterval] unless [force] is true. + Future _performBidirectionalSync({bool force = false}) async { + if (_serverManager.onlineClients.isEmpty) { + appLogger.d( + 'Skipping watch sync - no connected servers available yet', + ); + return; + } + + // Always push local changes to server (never throttle outbound sync) + await syncPendingItems(); + + // Only throttle the pull from server + if (!force && _lastSyncTime != null) { + final elapsed = DateTime.now().difference(_lastSyncTime!); + if (elapsed < minSyncInterval) { + appLogger.d( + 'Skipping server pull - last sync was ${elapsed.inMinutes}m ago (min: ${minSyncInterval.inMinutes}m)', + ); + return; + } + } + + // Pull latest states from server + await syncWatchStatesFromServer(); + _lastSyncTime = DateTime.now(); + } + + /// Called when app becomes active - syncs if interval has passed. + void onAppResumed() { + if (_offlineModeProvider?.isOffline != true) { + appLogger.d('App resumed - checking if sync needed'); + _performBidirectionalSync(); + } + } + + /// Called when servers connect on app startup. + /// + /// Triggers the initial sync now that PlexClients are available. + /// Only runs once per app session. + void onServersConnected() { + if (_hasPerformedStartupSync) return; + _hasPerformedStartupSync = true; + + if (_offlineModeProvider?.isOffline != true) { + appLogger.i('Servers connected - performing startup sync'); + _performBidirectionalSync(); + } + } + + /// Queue a progress update for later sync. + /// + /// This is called during offline playback to track the watch position. + /// If progress exceeds 90%, shouldMarkWatched is set to true. + Future queueProgressUpdate({ + required String serverId, + required String ratingKey, + required int viewOffset, + required int duration, + }) async { + final shouldMarkWatched = isWatchedByProgress(viewOffset, duration); + + await _database.upsertProgressAction( + serverId: serverId, + ratingKey: ratingKey, + viewOffset: viewOffset, + duration: duration, + shouldMarkWatched: shouldMarkWatched, + ); + + appLogger.d( + 'Queued offline progress: $serverId:$ratingKey at ${(viewOffset / 1000).toStringAsFixed(0)}s / ${(duration / 1000).toStringAsFixed(0)}s (${((viewOffset / duration) * 100).toStringAsFixed(1)}%)', + ); + + notifyListeners(); + } + + /// Queue a manual "mark as watched" action. + /// + /// Removes any conflicting actions for the same item. + Future queueMarkWatched({ + required String serverId, + required String ratingKey, + }) async { + await _database.insertWatchAction( + serverId: serverId, + ratingKey: ratingKey, + actionType: 'watched', + ); + + appLogger.d('Queued offline mark watched: $serverId:$ratingKey'); + notifyListeners(); + } + + /// Queue a manual "mark as unwatched" action. + /// + /// Removes any conflicting actions for the same item. + Future queueMarkUnwatched({ + required String serverId, + required String ratingKey, + }) async { + await _database.insertWatchAction( + serverId: serverId, + ratingKey: ratingKey, + actionType: 'unwatched', + ); + + appLogger.d('Queued offline mark unwatched: $serverId:$ratingKey'); + notifyListeners(); + } + + /// Check if an item should be considered watched based on progress percentage. + bool isWatchedByProgress(int viewOffset, int duration) { + if (duration == 0) return false; + return (viewOffset / duration) >= watchedThreshold; + } + + /// Get the local watch status for a media item. + /// + /// Returns: + /// - `true` if item was marked as watched locally or progress >= 90% + /// - `false` if item was marked as unwatched locally + /// - `null` if no local action exists (use cached server data) + Future getLocalWatchStatus(String globalKey) async { + final action = await _database.getLatestWatchAction(globalKey); + if (action == null) return null; + + switch (action.actionType) { + case 'watched': + return true; + case 'unwatched': + return false; + case 'progress': + // Check if progress exceeds threshold + return action.shouldMarkWatched; + default: + return null; + } + } + + /// Get the local view offset (resume position) for a media item. + /// + /// Returns the locally tracked position, or null if none exists. + Future getLocalViewOffset(String globalKey) async { + final action = await _database.getLatestWatchAction(globalKey); + if (action == null) return null; + + // Only return offset for progress actions + if (action.actionType == 'progress') { + return action.viewOffset; + } + + return null; + } + + /// Get count of pending sync items. + Future getPendingSyncCount() async { + return _database.getPendingSyncCount(); + } + + /// Sync all pending items to their respective servers. + /// + /// Called automatically when connectivity is restored, or manually. + Future syncPendingItems() async { + if (_isSyncing) { + appLogger.d('Sync already in progress, skipping'); + return; + } + + _isSyncing = true; + notifyListeners(); + + try { + final pendingActions = await _database.getPendingWatchActions(); + + if (pendingActions.isEmpty) { + appLogger.d('No pending watch actions to sync'); + return; + } + + appLogger.i('Syncing ${pendingActions.length} pending watch actions'); + + for (final action in pendingActions) { + // Delete items that have exceeded retry limit + if (action.syncAttempts >= maxSyncAttempts) { + appLogger.w( + 'Deleting action ${action.id} - exceeded retry limit ' + '(${action.syncAttempts} attempts). Last error: ${action.lastError}', + ); + await _database.deleteWatchAction(action.id); + continue; + } + + // Get the client for this server + final client = _serverManager.getClient(action.serverId); + if (client == null) { + // Check if this server still exists in the app + if (_serverManager.getServer(action.serverId) == null) { + appLogger.w( + 'Deleting action ${action.id} - server ${action.serverId} no longer exists', + ); + await _database.deleteWatchAction(action.id); + } else { + appLogger.w( + 'No client available for server ${action.serverId}, will retry', + ); + await _database.updateSyncAttempt( + action.id, + 'Server not available', + ); + } + continue; + } + + // Check if server is online + if (!_serverManager.isServerOnline(action.serverId)) { + appLogger.d('Server ${action.serverId} is offline, skipping'); + continue; + } + + try { + await _syncAction(client, action); + // Success - delete the action from queue + await _database.deleteWatchAction(action.id); + appLogger.d( + 'Successfully synced action ${action.id}: ${action.actionType} for ${action.ratingKey}', + ); + } catch (e) { + appLogger.w('Failed to sync action ${action.id}: $e'); + await _database.updateSyncAttempt(action.id, e.toString()); + } + } + } finally { + _isSyncing = false; + notifyListeners(); + } + } + + /// Sync a single action to the server. + Future _syncAction( + PlexClient client, + OfflineWatchProgressItem action, + ) async { + switch (action.actionType) { + case 'watched': + await client.markAsWatched(action.ratingKey); + break; + + case 'unwatched': + await client.markAsUnwatched(action.ratingKey); + break; + + case 'progress': + // First, update the timeline with current position + if (action.viewOffset != null && action.duration != null) { + await client.updateProgress( + action.ratingKey, + time: action.viewOffset!, + state: + 'stopped', // Use 'stopped' since we're syncing after the fact + duration: action.duration, + ); + } + + // If progress exceeded threshold, also mark as watched + if (action.shouldMarkWatched) { + await client.markAsWatched(action.ratingKey); + } + break; + } + } + + /// Fetch latest watch states from server and update local cache. + /// + /// Called when coming online or on app startup to pull any watch state + /// changes made on other devices. + /// + /// Optimized to fetch episodes by season (one API call per season) + /// instead of one API call per episode. + Future syncWatchStatesFromServer() async { + try { + // Get all downloaded items from database + final downloadedItems = await _database.getAllDownloadedMetadata(); + + if (downloadedItems.isEmpty) { + appLogger.d('No downloaded items to sync watch states for'); + return; + } + + appLogger.i( + 'Syncing watch states from server for ${downloadedItems.length} items', + ); + + // Separate episodes (with season parent) from other items (movies, etc.) + // Structure: serverId -> seasonRatingKey -> Set + final episodesByServerAndSeason = >>{}; + // Structure: serverId -> List + final nonEpisodeItems = >{}; + + for (final item in downloadedItems) { + if (item.type == 'episode' && item.parentRatingKey != null) { + // Group episodes by server and season for batch fetching + episodesByServerAndSeason + .putIfAbsent(item.serverId, () => {}) + .putIfAbsent(item.parentRatingKey!, () => {}) + .add(item.ratingKey); + } else { + // Movies, or episodes without parent (fallback to individual fetch) + nonEpisodeItems + .putIfAbsent(item.serverId, () => []) + .add(item.ratingKey); + } + } + + int syncedCount = 0; + int seasonCount = 0; + + // Fetch episodes by season (batch) - one API call per season + for (final serverEntry in episodesByServerAndSeason.entries) { + final serverId = serverEntry.key; + final seasonMap = serverEntry.value; + final client = _serverManager.getClient(serverId); + + if (client == null) { + appLogger.d('No client for server $serverId, skipping'); + continue; + } + + if (!_serverManager.isServerOnline(serverId)) { + appLogger.d('Server $serverId is offline, skipping'); + continue; + } + + for (final seasonEntry in seasonMap.entries) { + final seasonRatingKey = seasonEntry.key; + final downloadedEpisodeKeys = seasonEntry.value; + + try { + // Fetch all episodes in this season with one API call + final seasonEpisodes = await client.getChildren(seasonRatingKey); + seasonCount++; + + // Cache only the episodes we have downloaded + for (final episode in seasonEpisodes) { + if (downloadedEpisodeKeys.contains(episode.ratingKey)) { + await PlexApiCache.instance.put( + serverId, + '/library/metadata/${episode.ratingKey}', + { + 'MediaContainer': { + 'Metadata': [episode.toJson()], + }, + }, + ); + syncedCount++; + } + } + } catch (e) { + appLogger.d( + 'Failed to sync watch states for season $seasonRatingKey: $e', + ); + } + } + } + + // Fetch non-episode items individually (movies, etc.) + for (final entry in nonEpisodeItems.entries) { + final serverId = entry.key; + final ratingKeys = entry.value; + final client = _serverManager.getClient(serverId); + + if (client == null) { + appLogger.d('No client for server $serverId, skipping'); + continue; + } + + if (!_serverManager.isServerOnline(serverId)) { + appLogger.d('Server $serverId is offline, skipping'); + continue; + } + + for (final ratingKey in ratingKeys) { + try { + final metadata = await client.getMetadataWithImages(ratingKey); + if (metadata != null) { + await PlexApiCache.instance.put( + serverId, + '/library/metadata/$ratingKey', + { + 'MediaContainer': { + 'Metadata': [metadata.toJson()], + }, + }, + ); + syncedCount++; + } + } catch (e) { + appLogger.d('Failed to sync watch state for $ratingKey: $e'); + } + } + } + + final movieCount = nonEpisodeItems.values.fold(0, (a, b) => a + b.length); + appLogger.i( + 'Synced watch states: $seasonCount seasons, $movieCount other items ($syncedCount total)', + ); + + // Notify download provider to refresh metadata from updated cache + if (syncedCount > 0) { + onWatchStatesRefreshed?.call(); + } + + notifyListeners(); + } catch (e) { + appLogger.w('Error syncing watch states from server: $e'); + } + } + + /// Clear all pending watch actions (e.g., when logging out). + Future clearAll() async { + await _database.clearAllWatchActions(); + notifyListeners(); + } + + @override + void dispose() { + if (_offlineModeProvider != null && _offlineModeListener != null) { + _offlineModeProvider!.removeListener(_offlineModeListener!); + } + super.dispose(); + } +} diff --git a/lib/services/playback_initialization_service.dart b/lib/services/playback_initialization_service.dart index 5448fa50..d1317c5b 100644 --- a/lib/services/playback_initialization_service.dart +++ b/lib/services/playback_initialization_service.dart @@ -1,25 +1,147 @@ import 'plex_client.dart'; import '../models/plex_media_info.dart'; import '../models/plex_metadata.dart'; +import '../models/download_status.dart'; import '../mpv/mpv.dart'; import '../utils/app_logger.dart'; import '../i18n/strings.g.dart'; +import '../database/app_database.dart'; +import 'download_storage_service.dart'; +import 'dart:io'; +import 'package:drift/drift.dart'; /// Service responsible for fetching video playback data from the Plex server class PlaybackInitializationService { final PlexClient client; + final AppDatabase? database; - PlaybackInitializationService({required this.client}); + PlaybackInitializationService({required this.client, this.database}); + + /// Check if content is available offline and return local path + /// + /// Returns the local file path if the video is downloaded and completed. + /// Returns null if not available offline or database is not provided. + Future getOfflineVideoPath(String serverId, String ratingKey) async { + if (database == null) { + return null; + } + + try { + // Query database for downloaded media with matching serverId and ratingKey + final query = database!.select(database!.downloadedMedia) + ..where( + (tbl) => + tbl.serverId.equals(serverId) & tbl.ratingKey.equals(ratingKey), + ); + + final downloadedItem = await query.getSingleOrNull(); + + // Return null if not found or not completed + if (downloadedItem == null || + downloadedItem.status != DownloadStatus.completed.index) { + return null; + } + + // Return null if no video file path + if (downloadedItem.videoFilePath == null) { + return null; + } + + final storageService = DownloadStorageService.instance; + final storedPath = downloadedItem.videoFilePath!; + + // Check if this is a SAF URI (content://) + if (storageService.isSafUri(storedPath)) { + // SAF URIs are already valid for media players + appLogger.d('Found offline video (SAF): $storedPath'); + return storedPath; + } + + // Convert relative path to absolute path (handles both old absolute and new relative paths) + final absolutePath = await storageService.ensureAbsolutePath(storedPath); + + // Verify file exists + final file = File(absolutePath); + if (!await file.exists()) { + appLogger.w( + 'Offline video file not found: $absolutePath (stored as: $storedPath)', + ); + return null; + } + + appLogger.d('Found offline video: $absolutePath'); + return absolutePath; + } catch (e) { + appLogger.w('Error checking offline video path', error: e); + return null; + } + } /// Fetch playback data for the given metadata /// /// Returns a PlaybackInitializationResult with video URL and available versions + /// If [preferOffline] is true and offline content is available, uses local file Future getPlaybackData({ required PlexMetadata metadata, required int selectedMediaIndex, + bool preferOffline = false, }) async { try { - // Get consolidated playback data (URL, media info, and versions) in a single API call + // Check for offline content first if preferOffline is enabled + String? offlineVideoPath; + if (preferOffline && database != null) { + offlineVideoPath = await getOfflineVideoPath( + client.serverId, + metadata.ratingKey, + ); + } + + // If offline video is available, use it + if (offlineVideoPath != null) { + appLogger.d('Using offline playback for ${metadata.ratingKey}'); + + // For offline playback, we still need to fetch media info for subtitles + // but use the local file path for video + try { + final playbackData = await client.getVideoPlaybackData( + metadata.ratingKey, + mediaIndex: selectedMediaIndex, + ); + + // Build list of external subtitle tracks + final externalSubtitles = _buildExternalSubtitles( + playbackData.mediaInfo, + ); + + // Return result with local file path (file:// prefix for file paths, keep SAF URIs as-is) + return PlaybackInitializationResult( + availableVersions: playbackData.availableVersions, + videoUrl: offlineVideoPath.contains('://') + ? offlineVideoPath + : 'file://$offlineVideoPath', + mediaInfo: playbackData.mediaInfo, + externalSubtitles: externalSubtitles, + isOffline: true, + ); + } catch (e) { + // If we can't fetch media info (e.g., no network), use offline-only mode + appLogger.w( + 'Failed to fetch media info for offline video, using offline-only mode', + error: e, + ); + return PlaybackInitializationResult( + availableVersions: [], + videoUrl: offlineVideoPath.contains('://') + ? offlineVideoPath + : 'file://$offlineVideoPath', + mediaInfo: null, + externalSubtitles: const [], + isOffline: true, + ); + } + } + + // Fall back to network streaming final playbackData = await client.getVideoPlaybackData( metadata.ratingKey, mediaIndex: selectedMediaIndex, @@ -38,6 +160,7 @@ class PlaybackInitializationService { videoUrl: playbackData.videoUrl, mediaInfo: playbackData.mediaInfo, externalSubtitles: externalSubtitles, + isOffline: false, ); } catch (e) { if (e is PlaybackException) { @@ -106,12 +229,14 @@ class PlaybackInitializationResult { final String? videoUrl; final PlexMediaInfo? mediaInfo; final List externalSubtitles; + final bool isOffline; PlaybackInitializationResult({ required this.availableVersions, this.videoUrl, this.mediaInfo, this.externalSubtitles = const [], + this.isOffline = false, }); } diff --git a/lib/services/playback_progress_tracker.dart b/lib/services/playback_progress_tracker.dart index 999bb097..bdccf8b4 100644 --- a/lib/services/playback_progress_tracker.dart +++ b/lib/services/playback_progress_tracker.dart @@ -3,20 +3,33 @@ import 'dart:async'; import '../mpv/mpv.dart'; import 'plex_client.dart'; +import 'offline_watch_sync_service.dart'; import '../models/plex_metadata.dart'; import '../utils/app_logger.dart'; /// Tracks playback progress and reports it to the Plex server. /// /// Handles: -/// - Periodic timeline updates during playback +/// - Periodic timeline updates during playback (online) or queuing (offline) /// - Resume position tracking /// - State change reporting (playing, paused, stopped) +/// - Offline progress queuing for later sync class PlaybackProgressTracker { - final PlexClient client; + /// Plex client for online progress updates (null when offline) + final PlexClient? client; + + /// Metadata of the media being played final PlexMetadata metadata; + + /// Video player instance final Player player; + /// Whether playback is in offline mode + final bool isOffline; + + /// Service for queuing offline progress updates + final OfflineWatchSyncService? offlineWatchService; + /// Timer for periodic progress updates Timer? _progressTimer; @@ -27,12 +40,22 @@ class PlaybackProgressTracker { required this.client, required this.metadata, required this.player, + this.isOffline = false, + this.offlineWatchService, this.updateInterval = const Duration(seconds: 10), - }); + }) : assert( + !isOffline || offlineWatchService != null, + 'offlineWatchService is required when isOffline is true', + ), + assert( + isOffline || client != null, + 'client is required when isOffline is false', + ); /// Start tracking playback progress /// - /// Begins periodic timeline updates to the Plex server. + /// Begins periodic timeline updates to the Plex server (online) + /// or queuing progress updates locally (offline). void startTracking() { if (_progressTimer != null) { appLogger.w('Progress tracking already started'); @@ -46,7 +69,7 @@ class PlaybackProgressTracker { }); appLogger.d( - 'Started progress tracking (interval: ${updateInterval.inSeconds}s)', + 'Started progress tracking (interval: ${updateInterval.inSeconds}s, offline: $isOffline)', ); } @@ -59,7 +82,7 @@ class PlaybackProgressTracker { appLogger.d('Stopped progress tracking'); } - /// Send progress update to Plex server + /// Send progress update to Plex server or queue locally /// /// [state] can be 'playing', 'paused', or 'stopped' Future sendProgress(String state) async { @@ -76,44 +99,60 @@ class PlaybackProgressTracker { return; } - // Build timeline query parameters - final queryParams = { - 'ratingKey': metadata.ratingKey, - 'key': '/library/metadata/${metadata.ratingKey}', - 'state': state, - 'time': position.inMilliseconds.toString(), - 'duration': duration.inMilliseconds.toString(), - }; - - // Add playQueueItemID if available (for playlist/shuffle playback) - if (metadata.playQueueItemID != null) { - queryParams['playQueueItemID'] = metadata.playQueueItemID.toString(); + if (isOffline) { + // Queue progress update for later sync + await _sendOfflineProgress(position, duration); + } else { + // Send progress to server immediately + await _sendOnlineProgress(state, position, duration); } - - // Send timeline update - await client - .updateProgress( - metadata.ratingKey, - time: position.inMilliseconds, - state: state, - duration: duration.inMilliseconds, - ) - .catchError((error) { - // Silent handling - don't interrupt playback for progress errors - appLogger.d( - 'Failed to update progress (non-critical)', - error: error, - ); - }); - - appLogger.d( - 'Progress update sent: $state at ${position.inSeconds}s / ${duration.inSeconds}s', - ); } catch (e) { appLogger.d('Failed to send progress update (non-critical)', error: e); } } + /// Send progress update to Plex server (online mode) + Future _sendOnlineProgress( + String state, + Duration position, + Duration duration, + ) async { + await client!.updateProgress( + metadata.ratingKey, + time: position.inMilliseconds, + state: state, + duration: duration.inMilliseconds, + ); + + appLogger.d( + 'Progress update sent: $state at ${position.inSeconds}s / ${duration.inSeconds}s', + ); + } + + /// Queue progress update locally (offline mode) + Future _sendOfflineProgress( + Duration position, + Duration duration, + ) async { + final serverId = metadata.serverId; + if (serverId == null) { + appLogger.w('Cannot queue offline progress: serverId is null'); + return; + } + + await offlineWatchService!.queueProgressUpdate( + serverId: serverId, + ratingKey: metadata.ratingKey, + viewOffset: position.inMilliseconds, + duration: duration.inMilliseconds, + ); + + final percent = (position.inMilliseconds / duration.inMilliseconds * 100); + appLogger.d( + 'Offline progress queued: ${position.inSeconds}s / ${duration.inSeconds}s (${percent.toStringAsFixed(1)}%)', + ); + } + /// Dispose resources void dispose() { stopTracking(); diff --git a/lib/services/plex_api_cache.dart b/lib/services/plex_api_cache.dart new file mode 100644 index 00000000..f89604c9 --- /dev/null +++ b/lib/services/plex_api_cache.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +import '../database/app_database.dart'; + +/// Key-value cache for Plex API responses using Drift/SQLite. +/// Stores raw JSON responses keyed by serverId:endpoint format. +class PlexApiCache { + static PlexApiCache? _instance; + static PlexApiCache get instance { + if (_instance == null) { + throw StateError( + 'PlexApiCache not initialized. Call PlexApiCache.initialize() first.', + ); + } + return _instance!; + } + + final AppDatabase _db; + + PlexApiCache._(this._db); + + /// Initialize the singleton with an AppDatabase instance + static void initialize(AppDatabase db) { + _instance = PlexApiCache._(db); + } + + /// Get the database instance (for services that need direct database access) + AppDatabase get database => _db; + + /// Build cache key from serverId and endpoint + String _buildKey(String serverId, String endpoint) { + return '$serverId:$endpoint'; + } + + /// Get cached response for an endpoint + Future?> get(String serverId, String endpoint) async { + final key = _buildKey(serverId, endpoint); + final result = await (_db.select( + _db.apiCache, + )..where((t) => t.cacheKey.equals(key))).getSingleOrNull(); + + if (result != null) { + return jsonDecode(result.data) as Map; + } + return null; + } + + /// Cache a response for an endpoint + Future put( + String serverId, + String endpoint, + Map data, + ) async { + final key = _buildKey(serverId, endpoint); + await _db + .into(_db.apiCache) + .insertOnConflictUpdate( + ApiCacheCompanion( + cacheKey: Value(key), + data: Value(jsonEncode(data)), + cachedAt: Value(DateTime.now()), + ), + ); + } + + /// Delete all cached data for a server + Future deleteForServer(String serverId) async { + await (_db.delete( + _db.apiCache, + )..where((t) => t.cacheKey.like('$serverId:%'))).go(); + } + + /// Delete cached data for a specific item (when removing a download) + Future deleteForItem(String serverId, String ratingKey) async { + // Delete the metadata endpoint + final metadataKey = _buildKey(serverId, '/library/metadata/$ratingKey'); + final childrenKey = _buildKey( + serverId, + '/library/metadata/$ratingKey/children', + ); + + await (_db.delete(_db.apiCache)..where( + (t) => + t.cacheKey.equals(metadataKey) | t.cacheKey.equals(childrenKey), + )) + .go(); + } + + /// Mark an item as pinned for offline access + Future pinForOffline(String serverId, String ratingKey) async { + final metadataKey = _buildKey(serverId, '/library/metadata/$ratingKey'); + await (_db.update(_db.apiCache) + ..where((t) => t.cacheKey.equals(metadataKey))) + .write(const ApiCacheCompanion(pinned: Value(true))); + } + + /// Unpin an item + Future unpinForOffline(String serverId, String ratingKey) async { + final metadataKey = _buildKey(serverId, '/library/metadata/$ratingKey'); + await (_db.update(_db.apiCache) + ..where((t) => t.cacheKey.equals(metadataKey))) + .write(const ApiCacheCompanion(pinned: Value(false))); + } + + /// Check if an item is pinned for offline + Future isPinned(String serverId, String ratingKey) async { + final metadataKey = _buildKey(serverId, '/library/metadata/$ratingKey'); + final result = await (_db.select( + _db.apiCache, + )..where((t) => t.cacheKey.equals(metadataKey))).getSingleOrNull(); + return result?.pinned ?? false; + } + + /// Get all pinned rating keys for a server + Future> getPinnedKeys(String serverId) async { + final results = + await (_db.select(_db.apiCache)..where( + (t) => t.cacheKey.like('$serverId:%') & t.pinned.equals(true), + )) + .get(); + + final keys = {}; + for (final row in results) { + // Extract ratingKey from cache key like "serverId:/library/metadata/12345" + // Rating keys can be alphanumeric, not just numeric + final match = RegExp( + r'/library/metadata/([^/]+)$', + ).firstMatch(row.cacheKey); + if (match != null) { + keys.add(match.group(1)!); + } + } + return keys; + } + + /// Clear all cached data (useful for debugging/testing) + Future clearAll() async { + await _db.delete(_db.apiCache).go(); + } +} diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index e8938288..4cebd187 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -17,6 +17,7 @@ import '../models/plex_video_playback_data.dart'; import '../utils/endpoint_failover_interceptor.dart'; import '../utils/app_logger.dart'; import '../utils/log_redaction_manager.dart'; +import 'plex_api_cache.dart'; /// Result of testing a connection, including success status and latency class ConnectionTestResult { @@ -38,6 +39,20 @@ class PlexClient { /// Server name - all PlexMetadata items created by this client are tagged with this final String? serverName; + /// API response cache for offline support + final PlexApiCache _cache = PlexApiCache.instance; + + /// Whether to operate in offline mode (use cache only) + bool _offlineMode = false; + + /// Set offline mode - when true, only cached responses are returned + void setOfflineMode(bool offline) { + _offlineMode = offline; + } + + /// Get current offline mode state + bool get isOfflineMode => _offlineMode; + /// Custom response decoder that handles malformed UTF-8 gracefully static String _lenientUtf8Decoder( List responseBytes, @@ -249,17 +264,6 @@ class PlexClient { return null; } - /// Extract single PlexMetadata from response (returns first item or null) - /// Automatically tags the item with this client's serverId and serverName - PlexMetadata? _extractSingleMetadata(Response response) { - final metadataJson = _getFirstMetadataJson(response); - return metadataJson != null - ? PlexMetadata.fromJson( - metadataJson, - ).copyWith(serverId: serverId, serverName: serverName) - : null; - } - /// Generic helper to extract and map Directory list from response List _extractDirectoryList( Response response, @@ -347,10 +351,21 @@ class PlexClient { return _extractMetadataList(response); } - /// Get metadata by rating key - Future getMetadata(String ratingKey) async { - final response = await _dio.get('/library/metadata/$ratingKey'); - return _extractSingleMetadata(response); + /// Parse list of PlexMetadata from a cached response + List _parseMetadataListFromCachedResponse( + Map cached, + ) { + if (cached['MediaContainer'] != null && + cached['MediaContainer']['Metadata'] != null) { + return (cached['MediaContainer']['Metadata'] as List) + .map( + (json) => PlexMetadata.fromJson( + json, + ).copyWith(serverId: serverId, serverName: serverName), + ) + .toList(); + } + return []; } /// Get the server's machine identifier @@ -378,54 +393,146 @@ class PlexClient { } /// Get metadata by rating key with images (includes clearLogo and OnDeck) + /// Uses cache when offline or as fallback on network error + /// Note: OnDeck data is not relevant for offline mode + /// Always fetches with chapters/markers but caches at base endpoint Future> getMetadataWithImagesAndOnDeck( String ratingKey, ) async { - final response = await _dio.get( - '/library/metadata/$ratingKey', - queryParameters: {'includeOnDeck': 1}, - ); + // Cache key is always the base endpoint (no query params) + final cacheKey = '/library/metadata/$ratingKey'; - PlexMetadata? metadata; - PlexMetadata? onDeckEpisode; + // If offline mode, return from cache only (no OnDeck in offline) + if (_offlineMode) { + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + final metadata = _parseMetadataWithImagesFromCachedResponse(cached); + return {'metadata': metadata, 'onDeckEpisode': null}; + } + return {'metadata': null, 'onDeckEpisode': null}; + } - final metadataJson = _getFirstMetadataJson(response); + // Online: try network first + try { + // Fetch with chapters/markers/onDeck + final response = await _dio.get( + '/library/metadata/$ratingKey', + queryParameters: { + 'includeChapters': 1, + 'includeMarkers': 1, + 'includeOnDeck': 1, + }, + ); - // Log the parsed metadata JSON - if (metadataJson != null) { - metadata = PlexMetadata.fromJsonWithImages( - metadataJson, - ).copyWith(serverId: serverId, serverName: serverName); + // Cache at base endpoint (single cache entry per item) + if (response.data != null) { + await _cache.put(serverId, cacheKey, response.data); + } - // Check if OnDeck is nested inside Metadata - if (metadataJson.containsKey('OnDeck') && - metadataJson['OnDeck'] != null) { - final onDeckData = metadataJson['OnDeck']; + PlexMetadata? metadata; + PlexMetadata? onDeckEpisode; - // OnDeck can be either a Map with 'Metadata' key or direct metadata - if (onDeckData is Map && onDeckData.containsKey('Metadata')) { - final onDeckMetadata = onDeckData['Metadata']; - if (onDeckMetadata != null) { - onDeckEpisode = PlexMetadata.fromJson( - onDeckMetadata, - ).copyWith(serverId: serverId, serverName: serverName); + final metadataJson = _getFirstMetadataJson(response); + + if (metadataJson != null) { + metadata = PlexMetadata.fromJsonWithImages( + metadataJson, + ).copyWith(serverId: serverId, serverName: serverName); + + // Check if OnDeck is nested inside Metadata + if (metadataJson.containsKey('OnDeck') && + metadataJson['OnDeck'] != null) { + final onDeckData = metadataJson['OnDeck']; + + // OnDeck can be either a Map with 'Metadata' key or direct metadata + if (onDeckData is Map && onDeckData.containsKey('Metadata')) { + final onDeckMetadata = onDeckData['Metadata']; + if (onDeckMetadata != null) { + onDeckEpisode = PlexMetadata.fromJson( + onDeckMetadata, + ).copyWith(serverId: serverId, serverName: serverName); + } } } } - } - return {'metadata': metadata, 'onDeckEpisode': onDeckEpisode}; + return {'metadata': metadata, 'onDeckEpisode': onDeckEpisode}; + } catch (e) { + // Network failed - try cache as fallback (no OnDeck) + appLogger.w( + 'Network request failed for metadata with OnDeck, trying cache', + error: e, + ); + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + final metadata = _parseMetadataWithImagesFromCachedResponse(cached); + return {'metadata': metadata, 'onDeckEpisode': null}; + } + rethrow; + } } /// Get metadata by rating key with images (includes clearLogo) + /// Uses cache when offline or as fallback on network error + /// Always fetches with chapters/markers but caches at base endpoint Future getMetadataWithImages(String ratingKey) async { - final response = await _dio.get('/library/metadata/$ratingKey'); - final metadataJson = _getFirstMetadataJson(response); - return metadataJson != null - ? PlexMetadata.fromJsonWithImages( - metadataJson, - ).copyWith(serverId: serverId, serverName: serverName) - : null; + // Cache key is always the base endpoint (no query params) + final cacheKey = '/library/metadata/$ratingKey'; + + // If offline mode, return from cache only + if (_offlineMode) { + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + return _parseMetadataWithImagesFromCachedResponse(cached); + } + return null; + } + + // Online: try network first + try { + // Always fetch with chapters/markers - they're included in cached response + final response = await _dio.get( + '/library/metadata/$ratingKey', + queryParameters: {'includeChapters': 1, 'includeMarkers': 1}, + ); + + // Cache at base endpoint (single cache entry per item) + if (response.data != null) { + await _cache.put(serverId, cacheKey, response.data); + } + + final metadataJson = _getFirstMetadataJson(response); + return metadataJson != null + ? PlexMetadata.fromJsonWithImages( + metadataJson, + ).copyWith(serverId: serverId, serverName: serverName) + : null; + } catch (e) { + // Network failed - try cache as fallback + appLogger.w( + 'Network request failed for metadata with images, trying cache', + error: e, + ); + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + return _parseMetadataWithImagesFromCachedResponse(cached); + } + rethrow; + } + } + + /// Parse PlexMetadata with images from a cached response + PlexMetadata? _parseMetadataWithImagesFromCachedResponse( + Map cached, + ) { + if (cached['MediaContainer'] != null && + cached['MediaContainer']['Metadata'] != null && + (cached['MediaContainer']['Metadata'] as List).isNotEmpty) { + return PlexMetadata.fromJsonWithImages( + cached['MediaContainer']['Metadata'][0], + ).copyWith(serverId: serverId, serverName: serverName); + } + return null; } /// Set per-media language preferences (audio and subtitle) @@ -614,9 +721,41 @@ class PlexClient { } /// Get children of a metadata item (e.g., seasons for a show, episodes for a season) + /// Uses cache when offline or as fallback on network error Future> getChildren(String ratingKey) async { - final response = await _dio.get('/library/metadata/$ratingKey/children'); - return _extractMetadataList(response); + final endpoint = '/library/metadata/$ratingKey/children'; + + // If offline mode, return from cache only + if (_offlineMode) { + final cached = await _cache.get(serverId, endpoint); + if (cached != null) { + return _parseMetadataListFromCachedResponse(cached); + } + return []; + } + + // Online: try network first + try { + final response = await _dio.get(endpoint); + + // Cache the successful response + if (response.data != null) { + await _cache.put(serverId, endpoint, response.data); + } + + return _extractMetadataList(response); + } catch (e) { + // Network failed - try cache as fallback + appLogger.w( + 'Network request failed for children, trying cache', + error: e, + ); + final cached = await _cache.get(serverId, endpoint); + if (cached != null) { + return _parseMetadataListFromCachedResponse(cached); + } + rethrow; + } } /// Get all unwatched episodes for a TV show across all seasons @@ -749,6 +888,118 @@ class PlexClient { return []; } + /// Get chapters and markers from cached metadata or fetch if needed + /// Uses same cache key as other metadata methods for consistency + Future getPlaybackExtras(String ratingKey) async { + // Use same cache key as other metadata methods + final cacheKey = '/library/metadata/$ratingKey'; + + // If offline mode, return from cache only + if (_offlineMode) { + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + return _parsePlaybackExtrasFromCachedResponse(cached); + } + return PlaybackExtras(chapters: [], markers: []); + } + + // Online: check cache first (already has chapters/markers from other fetches) + appLogger.d('getPlaybackExtras: serverId=$serverId, cacheKey=$cacheKey'); + final cached = await _cache.get(serverId, cacheKey); + if (cached != null) { + final chapters = + cached['MediaContainer']?['Metadata']?[0]?['Chapter'] as List?; + appLogger.d( + 'getPlaybackExtras: cache hit, ${chapters?.length ?? 0} chapters', + ); + return _parsePlaybackExtrasFromCachedResponse(cached); + } + appLogger.d('getPlaybackExtras: cache miss'); + + // Not in cache - fetch and cache + try { + final response = await _dio.get( + '/library/metadata/$ratingKey', + queryParameters: {'includeChapters': 1, 'includeMarkers': 1}, + ); + + // Cache at base endpoint + if (response.data != null) { + await _cache.put(serverId, cacheKey, response.data); + } + + return _parsePlaybackExtrasFromResponse(response); + } catch (e) { + // Network failed + appLogger.w('Network request failed for playback extras', error: e); + return PlaybackExtras(chapters: [], markers: []); + } + } + + /// Parse PlaybackExtras from API response + PlaybackExtras _parsePlaybackExtrasFromResponse(Response response) { + final metadataJson = _getFirstMetadataJson(response); + return _parsePlaybackExtrasFromMetadataJson(metadataJson); + } + + /// Parse PlaybackExtras from cached response + PlaybackExtras _parsePlaybackExtrasFromCachedResponse( + Map cached, + ) { + Map? metadataJson; + if (cached['MediaContainer'] != null && + cached['MediaContainer']['Metadata'] != null && + (cached['MediaContainer']['Metadata'] as List).isNotEmpty) { + metadataJson = + cached['MediaContainer']['Metadata'][0] as Map; + } + return _parsePlaybackExtrasFromMetadataJson(metadataJson); + } + + /// Parse PlaybackExtras from metadata JSON + PlaybackExtras _parsePlaybackExtrasFromMetadataJson( + Map? metadataJson, + ) { + final chapters = []; + final markers = []; + + if (metadataJson != null) { + // Parse chapters + if (metadataJson['Chapter'] != null) { + final chapterList = metadataJson['Chapter'] as List; + for (var chapter in chapterList) { + chapters.add( + PlexChapter( + id: chapter['id'] as int, + index: chapter['index'] as int?, + startTimeOffset: chapter['startTimeOffset'] as int?, + endTimeOffset: chapter['endTimeOffset'] as int?, + title: chapter['tag'] as String?, + thumb: chapter['thumb'] as String?, + ), + ); + } + } + + // Parse markers + if (metadataJson['Marker'] != null) { + final markerList = metadataJson['Marker'] as List; + for (var marker in markerList) { + markers.add( + PlexMarker( + id: marker['id'] as int, + type: marker['type'] as String, + startTimeOffset: marker['startTimeOffset'] as int, + endTimeOffset: marker['endTimeOffset'] as int, + ), + ); + } + } + } + + return PlaybackExtras(chapters: chapters, markers: markers); + } + /// Get detailed media info including chapters and tracks /// [mediaIndex] specifies which Media item to use (defaults to 0 - first version) Future getMediaInfo( diff --git a/lib/services/saf_storage_service.dart b/lib/services/saf_storage_service.dart new file mode 100644 index 00000000..ad7c8606 --- /dev/null +++ b/lib/services/saf_storage_service.dart @@ -0,0 +1,233 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:saf_util/saf_util.dart'; +import 'package:saf_util/saf_util_platform_interface.dart'; +import 'package:saf_stream/saf_stream.dart'; + +/// Handles Storage Access Framework (SAF) operations for Android +class SafStorageService { + static SafStorageService? _instance; + static SafStorageService get instance => _instance ??= SafStorageService._(); + SafStorageService._(); + + final SafUtil _safUtil = SafUtil(); + final SafStream _safStream = SafStream(); + + /// Check if SAF is available (Android only) + bool get isAvailable => Platform.isAndroid; + + /// Pick a directory using SAF + /// Returns the content:// URI or null if cancelled + Future pickDirectory() async { + if (!isAvailable) return null; + try { + // Pick directory with persistent write permission + final doc = await _safUtil.pickDirectory( + writePermission: true, + persistablePermission: true, + ); + return doc?.uri; + } catch (e) { + debugPrint('SAF pickDirectory error: $e'); + return null; + } + } + + /// Check if we have persisted access to a URI + Future hasPersistedPermission(String contentUri) async { + if (!isAvailable) return false; + try { + return await _safUtil.hasPersistedPermission( + contentUri, + checkRead: true, + checkWrite: true, + ); + } catch (e) { + debugPrint('SAF hasPersistedPermission error: $e'); + return false; + } + } + + /// Get document file info for a URI + Future getDocumentFile( + String contentUri, { + bool isDir = true, + }) async { + if (!isAvailable) return null; + try { + return await _safUtil.documentFileFromUri(contentUri, isDir); + } catch (e) { + debugPrint('SAF getDocumentFile error: $e'); + return null; + } + } + + /// Create a subdirectory in a SAF directory + /// Returns the URI of the created directory + Future createDirectory(String parentUri, String name) async { + if (!isAvailable) return null; + try { + final result = await _safUtil.mkdirp(parentUri, [name]); + return result.uri; + } catch (e) { + debugPrint('SAF createDirectory error: $e'); + return null; + } + } + + /// List files in a SAF directory + Future> listDirectory(String contentUri) async { + if (!isAvailable) return []; + try { + return await _safUtil.list(contentUri); + } catch (e) { + debugPrint('SAF listDirectory error: $e'); + return []; + } + } + + /// Get a child file/directory in a SAF directory + Future getChild(String parentUri, String name) async { + if (!isAvailable) return null; + try { + return await _safUtil.child(parentUri, [name]); + } catch (e) { + debugPrint('SAF getChild error: $e'); + return null; + } + } + + /// Delete a file or directory in SAF + Future delete(String contentUri, {bool isDir = false}) async { + if (!isAvailable) return false; + try { + await _safUtil.delete(contentUri, isDir); + return true; + } catch (e) { + debugPrint('SAF delete error: $e'); + return false; + } + } + + /// Get a display name for a SAF URI (for UI purposes) + Future getDisplayName(String contentUri) async { + if (!isAvailable) return null; + try { + final doc = await _safUtil.documentFileFromUri(contentUri, true); + return doc?.name; + } catch (e) { + debugPrint('SAF getDisplayName error: $e'); + return null; + } + } + + /// Create nested directories in a SAF directory + /// Returns the URI of the deepest directory + Future createNestedDirectories( + String parentUri, + List pathComponents, + ) async { + if (!isAvailable) return null; + try { + final result = await _safUtil.mkdirp(parentUri, pathComponents); + return result.uri; + } catch (e) { + debugPrint('SAF createNestedDirectories error: $e'); + return null; + } + } + + /// Copy a file from local storage to SAF directory using streaming + /// Returns the SAF URI of the copied file, or null on failure + Future copyFileToSaf( + String sourceFilePath, + String targetDirectoryUri, + String fileName, + String mimeType, + ) async { + if (!isAvailable) return null; + try { + final sourceFile = File(sourceFilePath); + if (!await sourceFile.exists()) { + debugPrint('SAF copyFileToSaf: source file does not exist'); + return null; + } + + // Start a write stream session + final writeInfo = await _safStream.startWriteStream( + targetDirectoryUri, + fileName, + mimeType, + ); + final sessionId = writeInfo.session; + + final stream = sourceFile.openRead(); + + await for (final chunk in stream) { + await _safStream.writeChunk(sessionId, Uint8List.fromList(chunk)); + } + + // End the write stream + await _safStream.endWriteStream(sessionId); + + debugPrint( + 'SAF copyFileToSaf: successfully copied to $targetDirectoryUri/$fileName', + ); + return writeInfo.fileResult.uri.toString(); + } catch (e) { + debugPrint('SAF copyFileToSaf error: $e'); + return null; + } + } + + /// Write bytes directly to a SAF file + /// Returns the SAF URI of the created file, or null on failure + Future writeFileBytes( + String directoryUri, + String fileName, + String mimeType, + Uint8List bytes, + ) async { + if (!isAvailable) return null; + try { + final result = await _safStream.writeFileBytes( + directoryUri, + fileName, + mimeType, + bytes, + ); + return result.uri.toString(); + } catch (e) { + debugPrint('SAF writeFileBytes error: $e'); + return null; + } + } + + /// Read bytes from a SAF file + Future readFileBytes(String fileUri) async { + if (!isAvailable) return null; + try { + return await _safStream.readFileBytes(fileUri); + } catch (e) { + debugPrint('SAF readFileBytes error: $e'); + return null; + } + } + + /// Check if a file exists in a SAF directory + Future fileExists(String parentUri, String fileName) async { + if (!isAvailable) return false; + try { + final child = await _safUtil.child(parentUri, [fileName]); + return child != null; + } catch (e) { + debugPrint('SAF fileExists error: $e'); + return false; + } + } + + /// Get the content URI for a file that should be readable by MediaStore/media players + /// For SAF files, this returns the same URI as input (content:// URIs are already readable) + String getReadableUri(String safUri) => safUri; +} diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 54d7ab14..3d85e5a9 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -45,6 +45,9 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyAutoSkipIntro = 'auto_skip_intro'; static const String _keyAutoSkipCredits = 'auto_skip_credits'; static const String _keyAutoSkipDelay = 'auto_skip_delay'; + static const String _keyCustomDownloadPath = 'custom_download_path'; + static const String _keyCustomDownloadPathType = 'custom_download_path_type'; + static const String _keyDownloadOnWifiOnly = 'download_on_wifi_only'; SettingsService._(); @@ -843,6 +846,41 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getInt(_keyAutoSkipDelay) ?? 5; // Default: 5 seconds } + // Custom Download Path + Future setCustomDownloadPath( + String? path, { + String type = 'file', + }) async { + if (path == null) { + await prefs.remove(_keyCustomDownloadPath); + await prefs.remove(_keyCustomDownloadPathType); + } else { + await prefs.setString(_keyCustomDownloadPath, path); + await prefs.setString(_keyCustomDownloadPathType, type); + } + } + + String? getCustomDownloadPath() { + return prefs.getString(_keyCustomDownloadPath); + } + + String getCustomDownloadPathType() { + return prefs.getString(_keyCustomDownloadPathType) ?? 'file'; + } + + bool hasCustomDownloadPath() { + return prefs.containsKey(_keyCustomDownloadPath); + } + + // Download on WiFi Only + Future setDownloadOnWifiOnly(bool value) async { + await prefs.setBool(_keyDownloadOnWifiOnly, value); + } + + bool getDownloadOnWifiOnly() { + return prefs.getBool(_keyDownloadOnWifiOnly) ?? false; + } + // Reset all settings to defaults Future resetAllSettings() async { await Future.wait([ @@ -874,6 +912,9 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keySubtitleBackgroundOpacity), prefs.remove(_keyAppLocale), prefs.remove(_keyRememberTrackSelections), + prefs.remove(_keyCustomDownloadPath), + prefs.remove(_keyCustomDownloadPathType), + prefs.remove(_keyDownloadOnWifiOnly), ]); } diff --git a/lib/utils/plex_image_helper.dart b/lib/utils/plex_image_helper.dart index 02ef996e..f82bfc31 100644 --- a/lib/utils/plex_image_helper.dart +++ b/lib/utils/plex_image_helper.dart @@ -120,8 +120,9 @@ class PlexImageHelper { /// Creates an optimized image URL for Plex content /// Falls back to original URL if transcoding is not appropriate + /// If client is null (offline mode), returns empty string for relative paths static String getOptimizedImageUrl({ - required PlexClient client, + PlexClient? client, required String? thumbPath, required double maxWidth, required double maxHeight, @@ -140,6 +141,12 @@ class PlexImageHelper { return basePath; } + // If no client (offline mode), we can't build URLs for relative paths + // Images should already be cached from when they were originally loaded + if (client == null) { + return ''; + } + // For art/backgrounds and clear logos, prefer the original image to avoid // any aspect ratio changes from Plex photo transcoding. if (imageType == ImageType.art || imageType == ImageType.logo) { diff --git a/lib/utils/smart_deletion_handler.dart b/lib/utils/smart_deletion_handler.dart new file mode 100644 index 00000000..e87a7b04 --- /dev/null +++ b/lib/utils/smart_deletion_handler.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/download_provider.dart'; +import '../widgets/deletion_progress_dialog.dart'; + +class SmartDeletionHandler { + /// Execute deletion with smart progress dialog + /// Only shows dialog if deletion takes longer than delayMs + static Future deleteWithProgress({ + required BuildContext context, + required DownloadProvider provider, + required String globalKey, + int delayMs = 500, + }) async { + bool dialogShown = false; + bool deletionComplete = false; + + // Start a timer to show dialog after delay + Future.delayed(Duration(milliseconds: delayMs), () { + if (!deletionComplete && context.mounted) { + dialogShown = true; + _showProgressDialog(context, provider, globalKey); + } + }); + + try { + // Start deletion + await provider.deleteDownload(globalKey); + deletionComplete = true; + + // Close dialog if shown + if (dialogShown && context.mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + deletionComplete = true; + + // Close dialog if shown + if (dialogShown && context.mounted) { + Navigator.of(context).pop(); + } + + rethrow; // Let caller handle error + } + } + + /// Show progress dialog and listen to updates + static void _showProgressDialog( + BuildContext context, + DownloadProvider provider, + String globalKey, + ) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => Consumer( + builder: (context, provider, child) { + final progress = provider.getDeletionProgress(globalKey); + + // If no progress, show simple fallback + if (progress == null) { + return const AlertDialog( + content: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(width: 20), + Text('Deleting...'), + ], + ), + ); + } + + return DeletionProgressDialog(progress: progress); + }, + ), + ); + } +} diff --git a/lib/utils/video_player_navigation.dart b/lib/utils/video_player_navigation.dart index f95623a6..2f1b51a1 100644 --- a/lib/utils/video_player_navigation.dart +++ b/lib/utils/video_player_navigation.dart @@ -21,6 +21,7 @@ import '../services/settings_service.dart'; /// loads the saved preference for the series/movie. Defaults to 0 if no preference exists. /// - [usePushReplacement]: If true, replaces current route instead of pushing; /// useful for episode-to-episode navigation. Defaults to false. +/// - [isOffline]: If true, plays from downloaded content without requiring server connection. /// /// Returns a Future that completes with a boolean indicating whether the content /// was watched, or null if navigation was cancelled. @@ -32,6 +33,7 @@ Future navigateToVideoPlayer( double? preferredPlaybackRate, int? selectedMediaIndex, bool usePushReplacement = false, + bool isOffline = false, }) async { // Extract navigator before any async operations final navigator = Navigator.of(context); @@ -60,6 +62,7 @@ Future navigateToVideoPlayer( preferredSubtitleTrack: preferredSubtitleTrack, preferredPlaybackRate: preferredPlaybackRate, selectedMediaIndex: mediaIndex, + isOffline: isOffline, ), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, diff --git a/lib/widgets/deletion_progress_dialog.dart b/lib/widgets/deletion_progress_dialog.dart new file mode 100644 index 00000000..c371b1b0 --- /dev/null +++ b/lib/widgets/deletion_progress_dialog.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import '../models/deletion_progress.dart'; +import '../i18n/strings.g.dart'; + +class DeletionProgressDialog extends StatelessWidget { + final DeletionProgress progress; + + const DeletionProgressDialog({super.key, required this.progress}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return PopScope( + canPop: false, // Prevent back button dismissal + child: AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Circular progress indicator + const SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator(), + ), + + const SizedBox(height: 24), + + // Progress text + Text( + t.downloads.deletingWithProgress( + title: progress.itemTitle, + current: progress.currentItem, + total: progress.totalItems, + ), + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + + const SizedBox(height: 24), + + // Progress text + Text( + 'Deleting ${progress.itemTitle}... (${progress.currentItem} of ${progress.totalItems})', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + + const SizedBox(height: 16), + + // Progress bar + LinearProgressIndicator(value: progress.progressPercent), + + const SizedBox(height: 8), + + // Percentage text + Text( + '${progress.progressPercentInt}%', + style: Theme.of(context).textTheme.bodySmall, + ), + + // Optional: Current operation + if (progress.currentOperation != null) ...[ + const SizedBox(height: 8), + Text( + progress.currentOperation!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).textTheme.bodySmall?.color?.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/widgets/download_tree_item.dart b/lib/widgets/download_tree_item.dart new file mode 100644 index 00000000..e4ddb48a --- /dev/null +++ b/lib/widgets/download_tree_item.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import '../models/download_progress.dart'; +import '../models/download_status.dart'; + +/// Represents a node type in the download tree +enum DownloadNodeType { show, season, episode, movie } + +/// Node in the download tree hierarchy +class DownloadTreeNode { + final String id; + final DownloadNodeType type; + final String title; + final DownloadProgress? progress; + final List children; + + // Episode-specific + final int? episodeNumber; + final int? seasonNumber; + + DownloadTreeNode({ + required this.id, + required this.type, + required this.title, + this.progress, + this.children = const [], + this.episodeNumber, + this.seasonNumber, + }); + + /// Calculate aggregate progress for shows/seasons + int get aggregateProgress { + if (progress != null) return progress!.progress; + if (children.isEmpty) return 0; + + int total = 0; + for (final child in children) { + total += child.aggregateProgress; + } + return (total / children.length).round(); + } + + /// Get the status - individual or aggregate + DownloadStatus get status { + if (progress != null) return progress!.status; + if (children.isEmpty) return DownloadStatus.queued; + + // Aggregate status from children + final statuses = children.map((c) => c.status).toSet(); + if (statuses.contains(DownloadStatus.downloading)) + return DownloadStatus.downloading; + if (statuses.contains(DownloadStatus.queued)) return DownloadStatus.queued; + if (statuses.contains(DownloadStatus.paused)) return DownloadStatus.paused; + if (statuses.contains(DownloadStatus.failed)) return DownloadStatus.failed; + if (statuses.every((s) => s == DownloadStatus.completed)) + return DownloadStatus.completed; + return DownloadStatus.queued; + } +} + +/// Widget that renders a single node in the download tree with radial progress indicator +class DownloadTreeItem extends StatelessWidget { + final DownloadTreeNode node; + final int depth; + final bool isExpanded; + final VoidCallback? onToggle; + final VoidCallback? onPause; + final VoidCallback? onCancel; + final VoidCallback? onDelete; + final VoidCallback? onResume; + final VoidCallback? onRetry; + + const DownloadTreeItem({ + super.key, + required this.node, + required this.depth, + this.isExpanded = false, + this.onToggle, + this.onPause, + this.onCancel, + this.onDelete, + this.onResume, + this.onRetry, + }); + + @override + Widget build(BuildContext context) { + final indentation = depth * 24.0; + + return Container( + padding: EdgeInsets.only(left: indentation), + child: InkWell( + onTap: node.children.isNotEmpty ? onToggle : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), + child: Row( + children: [ + // Expand/collapse icon for parent nodes + if (node.children.isNotEmpty) + _buildExpandIcon() + else if (node.type == DownloadNodeType.episode) + const SizedBox(width: 24), // Spacing for episodes + // Radial progress indicator + _buildRadialProgress(context), + + const SizedBox(width: 12), + + // Title and metadata + Expanded(child: _buildTitleSection(context)), + + // Action buttons + _buildActionButtons(context), + ], + ), + ), + ), + ); + } + + Widget _buildExpandIcon() { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Icon( + isExpanded ? Icons.expand_more : Icons.chevron_right, + size: 24, + ), + ); + } + + Widget _buildRadialProgress(BuildContext context) { + final size = _getProgressSize(); + final progress = node.aggregateProgress; + final color = _getStatusColor(node.status); + + return SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + // Background circle + CircularProgressIndicator( + value: 1.0, + strokeWidth: 3.0, + valueColor: AlwaysStoppedAnimation(color.withOpacity(0.2)), + ), + // Progress circle + CircularProgressIndicator( + value: progress / 100.0, + strokeWidth: 3.0, + valueColor: AlwaysStoppedAnimation(color), + ), + // Percentage text + Text( + '$progress%', + style: TextStyle( + fontSize: size > 30 ? 10 : 8, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + ], + ), + ); + } + + Widget _buildTitleSection(BuildContext context) { + switch (node.type) { + case DownloadNodeType.show: + return _buildShowTitle(context); + case DownloadNodeType.season: + return _buildSeasonTitle(context); + case DownloadNodeType.episode: + return _buildEpisodeTitle(context); + case DownloadNodeType.movie: + return _buildMovieTitle(context); + } + } + + Widget _buildShowTitle(BuildContext context) { + final episodeCount = node.children.fold( + 0, + (sum, season) => sum + season.children.length, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + Text( + '$episodeCount episode${episodeCount != 1 ? 's' : ''}', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ); + } + + Widget _buildSeasonTitle(BuildContext context) { + final seasonNum = node.seasonNumber ?? 1; + return Text( + 'Season $seasonNum', + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15), + ); + } + + Widget _buildEpisodeTitle(BuildContext context) { + final episodeNum = node.episodeNumber ?? 0; + final episodeLabel = episodeNum.toString().padLeft(2, '0'); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'E$episodeLabel - ${node.title}', + style: const TextStyle(fontSize: 14), + ), + if (node.progress != null && node.progress!.currentFile != null) + Text( + node.progress!.currentFile!, + style: TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ); + } + + Widget _buildMovieTitle(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + if (node.progress != null && node.progress!.currentFile != null) + Text( + node.progress!.currentFile!, + style: TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ); + } + + Widget _buildActionButtons(BuildContext context) { + // Only show action buttons for leaf nodes (episodes/movies) + if (node.type != DownloadNodeType.episode && + node.type != DownloadNodeType.movie) { + return const SizedBox.shrink(); + } + + final status = node.status; + + switch (status) { + case DownloadStatus.downloading: + return IconButton( + icon: const Icon(Icons.pause), + onPressed: onPause, + tooltip: 'Pause', + iconSize: 20, + ); + + case DownloadStatus.queued: + return IconButton( + icon: const Icon(Icons.close), + onPressed: onCancel, + tooltip: 'Cancel', + iconSize: 20, + ); + + case DownloadStatus.paused: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: onResume, + tooltip: 'Resume', + iconSize: 20, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: onCancel, + tooltip: 'Cancel', + iconSize: 20, + ), + ], + ); + + case DownloadStatus.completed: + return IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: onDelete, + tooltip: 'Delete', + iconSize: 20, + ); + + case DownloadStatus.failed: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: onRetry, + tooltip: 'Retry', + iconSize: 20, + ), + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: onDelete, + tooltip: 'Delete', + iconSize: 20, + ), + ], + ); + + case DownloadStatus.cancelled: + return IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: onDelete, + tooltip: 'Delete', + iconSize: 20, + ); + } + } + + double _getProgressSize() { + switch (node.type) { + case DownloadNodeType.show: + case DownloadNodeType.movie: + return 36.0; + case DownloadNodeType.season: + case DownloadNodeType.episode: + return 28.0; + } + } + + Color _getStatusColor(DownloadStatus status) { + switch (status) { + case DownloadStatus.downloading: + return Colors.blue; + case DownloadStatus.queued: + return Colors.orange; + case DownloadStatus.completed: + return Colors.green; + case DownloadStatus.failed: + return Colors.red; + case DownloadStatus.paused: + return Colors.amber; + case DownloadStatus.cancelled: + return Colors.grey; + } + } +} diff --git a/lib/widgets/download_tree_view.dart b/lib/widgets/download_tree_view.dart new file mode 100644 index 00000000..af723f3b --- /dev/null +++ b/lib/widgets/download_tree_view.dart @@ -0,0 +1,639 @@ +import 'package:flutter/material.dart'; +import '../i18n/strings.g.dart'; +import '../models/download_progress.dart'; +import '../models/download_status.dart'; +import '../models/plex_metadata.dart'; + +/// Represents a node in the download tree +class DownloadTreeNode { + final String key; + final String title; + final DownloadNodeType type; + final double progress; // 0.0-1.0 + final DownloadStatus status; + final List children; + final PlexMetadata? metadata; + final DownloadProgress? downloadProgress; + + const DownloadTreeNode({ + required this.key, + required this.title, + required this.type, + this.progress = 0.0, + required this.status, + this.children = const [], + this.metadata, + this.downloadProgress, + }); + + /// Check if this node has children + bool get hasChildren => children.isNotEmpty; + + /// Get the number of completed children + int get completedChildrenCount { + return children + .where((child) => child.status == DownloadStatus.completed) + .length; + } + + /// Get the number of downloading children + int get downloadingChildrenCount { + return children + .where((child) => child.status == DownloadStatus.downloading) + .length; + } +} + +/// Type of node in the download tree +enum DownloadNodeType { show, season, episode, movie } + +/// Hierarchical tree view for downloads +/// Groups TV shows by show -> season -> episode +/// Movies appear at top level +class DownloadTreeView extends StatefulWidget { + final Map downloads; + final Map metadata; + final void Function(String globalKey)? onPause; + final void Function(String globalKey)? onResume; + final void Function(String globalKey)? onRetry; + final void Function(String globalKey)? onCancel; + final void Function(String globalKey)? onDelete; + + const DownloadTreeView({ + super.key, + required this.downloads, + required this.metadata, + this.onPause, + this.onResume, + this.onRetry, + this.onCancel, + this.onDelete, + }); + + @override + State createState() => _DownloadTreeViewState(); +} + +class _DownloadTreeViewState extends State { + final Set _expandedNodes = {}; + + @override + Widget build(BuildContext context) { + final tree = _buildTree(); + final flattenedNodes = _flattenTree(tree); + + if (flattenedNodes.isEmpty) { + return const Center(child: Text('No downloads')); + } + + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: flattenedNodes.length, + itemBuilder: (context, index) { + final item = flattenedNodes[index]; + return _buildTreeItem(item.node, item.depth); + }, + ); + } + + /// Build the download tree from flat download list + List _buildTree() { + final Map>> showGroups = {}; + final List movies = []; + + // Group downloads + for (final entry in widget.downloads.entries) { + final globalKey = entry.key; + final download = entry.value; + final meta = widget.metadata[globalKey]; + + if (meta == null) continue; + + if (meta.type.toLowerCase() == 'episode') { + // Group episodes by show + final showKey = meta.grandparentRatingKey ?? 'unknown'; + showGroups.putIfAbsent(showKey, () => []); + showGroups[showKey]!.add(entry); + } else if (meta.type.toLowerCase() == 'movie') { + // Movies go at top level + movies.add( + DownloadTreeNode( + key: globalKey, + title: meta.title, + type: DownloadNodeType.movie, + progress: download.progressPercent, + status: download.status, + metadata: meta, + downloadProgress: download, + ), + ); + } + } + + // Build show nodes + final List shows = []; + for (final showEntry in showGroups.entries) { + final showKey = showEntry.key; + final episodes = showEntry.value; + + if (episodes.isEmpty) continue; + + // Get show metadata from first episode + final firstEpisode = widget.metadata[episodes.first.key]; + final showTitle = firstEpisode?.grandparentTitle ?? 'Unknown Show'; + + // Group episodes by season + final Map>> seasonGroups = + {}; + for (final episode in episodes) { + final meta = widget.metadata[episode.key]; + if (meta == null) continue; + + final seasonKey = meta.parentRatingKey ?? 'unknown'; + seasonGroups.putIfAbsent(seasonKey, () => []); + seasonGroups[seasonKey]!.add(episode); + } + + // Build season nodes + final List seasons = []; + for (final seasonEntry in seasonGroups.entries) { + final seasonKey = seasonEntry.key; + final seasonEpisodes = seasonEntry.value; + + if (seasonEpisodes.isEmpty) continue; + + // Get season metadata from first episode + final firstEpisode = widget.metadata[seasonEpisodes.first.key]; + final seasonTitle = firstEpisode?.parentTitle ?? 'Unknown Season'; + final seasonNumber = firstEpisode?.parentIndex; + + // Build episode nodes + final List episodeNodes = []; + for (final episodeEntry in seasonEpisodes) { + final globalKey = episodeEntry.key; + final download = episodeEntry.value; + final meta = widget.metadata[globalKey]; + + if (meta == null) continue; + + final episodeNumber = meta.index; + final episodeTitle = episodeNumber != null + ? 'Episode $episodeNumber - ${meta.title}' + : meta.title; + + episodeNodes.add( + DownloadTreeNode( + key: globalKey, + title: episodeTitle, + type: DownloadNodeType.episode, + progress: download.progressPercent, + status: download.status, + metadata: meta, + downloadProgress: download, + ), + ); + } + + // Sort episodes by episode number only (not by status) + episodeNodes.sort((a, b) { + final aIndex = a.metadata?.index ?? 0; + final bIndex = b.metadata?.index ?? 0; + return aIndex.compareTo(bIndex); + }); + + // Calculate aggregate season progress + final seasonProgress = episodeNodes.isEmpty + ? 0.0 + : episodeNodes.map((e) => e.progress).reduce((a, b) => a + b) / + episodeNodes.length; + final seasonStatus = _determineAggregateStatus( + episodeNodes.map((e) => e.status).toList(), + ); + + final displayTitle = seasonNumber != null + ? 'Season $seasonNumber' + : seasonTitle; + + seasons.add( + DownloadTreeNode( + key: '$showKey:$seasonKey', + title: displayTitle, + type: DownloadNodeType.season, + progress: seasonProgress, + status: seasonStatus, + children: episodeNodes, + ), + ); + } + + // Sort seasons by season number + seasons.sort((a, b) { + final aSeasonNum = + widget.metadata[a.children.first.key]?.parentIndex ?? 0; + final bSeasonNum = + widget.metadata[b.children.first.key]?.parentIndex ?? 0; + return aSeasonNum.compareTo(bSeasonNum); + }); + + // Calculate aggregate show progress + final showProgress = seasons.isEmpty + ? 0.0 + : seasons.map((s) => s.progress).reduce((a, b) => a + b) / + seasons.length; + final showStatus = _determineAggregateStatus( + seasons.map((s) => s.status).toList(), + ); + + shows.add( + DownloadTreeNode( + key: showKey, + title: showTitle, + type: DownloadNodeType.show, + progress: showProgress, + status: showStatus, + children: seasons, + ), + ); + } + + // Sort shows by status and title + shows.sort((a, b) { + final statusCompare = _compareByStatus(a.status, b.status); + if (statusCompare != 0) return statusCompare; + return a.title.compareTo(b.title); + }); + + // Sort movies by status and title + movies.sort((a, b) { + final statusCompare = _compareByStatus(a.status, b.status); + if (statusCompare != 0) return statusCompare; + return a.title.compareTo(b.title); + }); + + // Combine movies and shows + return [...movies, ...shows]; + } + + /// Determine aggregate status from child statuses + /// Priority: downloading > queued > paused > completed > failed + DownloadStatus _determineAggregateStatus(List statuses) { + if (statuses.isEmpty) return DownloadStatus.queued; + + if (statuses.any((s) => s == DownloadStatus.downloading)) { + return DownloadStatus.downloading; + } + if (statuses.any((s) => s == DownloadStatus.queued)) { + return DownloadStatus.queued; + } + if (statuses.any((s) => s == DownloadStatus.paused)) { + return DownloadStatus.paused; + } + if (statuses.any((s) => s == DownloadStatus.failed)) { + return DownloadStatus.failed; + } + return DownloadStatus.completed; + } + + /// Compare statuses for sorting (downloading first, then queued, etc.) + int _compareByStatus(DownloadStatus a, DownloadStatus b) { + const statusOrder = { + DownloadStatus.downloading: 0, + DownloadStatus.queued: 1, + DownloadStatus.paused: 2, + DownloadStatus.completed: 3, + DownloadStatus.failed: 4, + DownloadStatus.cancelled: 5, + }; + return (statusOrder[a] ?? 99).compareTo(statusOrder[b] ?? 99); + } + + /// Flatten the tree into a list of visible nodes with their depths + List<_FlatNode> _flattenTree(List nodes, [int depth = 0]) { + final List<_FlatNode> result = []; + + for (final node in nodes) { + result.add(_FlatNode(node: node, depth: depth)); + + // Add children if node is expanded + if (_expandedNodes.contains(node.key) && node.hasChildren) { + result.addAll(_flattenTree(node.children, depth + 1)); + } + } + + return result; + } + + /// Toggle node expansion + void _toggleExpansion(String key) { + setState(() { + if (_expandedNodes.contains(key)) { + _expandedNodes.remove(key); + } else { + _expandedNodes.add(key); + } + }); + } + + /// Build a tree item widget + Widget _buildTreeItem(DownloadTreeNode node, int depth) { + final isExpanded = _expandedNodes.contains(node.key); + final canExpand = node.hasChildren; + + return InkWell( + onTap: canExpand ? () => _toggleExpansion(node.key) : null, + child: Padding( + padding: EdgeInsets.only(left: depth * 16.0), + child: _buildNodeContent(node, isExpanded, canExpand), + ), + ); + } + + /// Build the content for a node + Widget _buildNodeContent( + DownloadTreeNode node, + bool isExpanded, + bool canExpand, + ) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // Expand/collapse icon + if (canExpand) + Icon(isExpanded ? Icons.expand_more : Icons.chevron_right, size: 20) + else + const SizedBox(width: 20), + + const SizedBox(width: 8), + + // Status icon + _buildStatusIcon(node.status), + + const SizedBox(width: 12), + + // Title and info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: canExpand ? FontWeight.w600 : FontWeight.normal, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + if (canExpand) ...[ + const SizedBox(height: 4), + Text( + _getNodeSummary(node), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + + // Progress bar + if (node.status == DownloadStatus.downloading || + node.status == DownloadStatus.queued) ...[ + const SizedBox(height: 8), + LinearProgressIndicator( + value: node.progress, + backgroundColor: theme.colorScheme.surfaceContainerHighest, + ), + if (node.downloadProgress != null) ...[ + const SizedBox(height: 4), + Text( + '${(node.progress * 100).toStringAsFixed(1)}% - ${node.downloadProgress!.speedFormatted}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ], + ], + ), + ), + + // Actions + _buildActions(node), + ], + ), + ); + } + + /// Build status icon + Widget _buildStatusIcon(DownloadStatus status) { + IconData iconData; + Color? color; + + switch (status) { + case DownloadStatus.downloading: + iconData = Icons.downloading; + color = Colors.blue; + break; + case DownloadStatus.queued: + iconData = Icons.schedule; + color = Colors.orange; + break; + case DownloadStatus.paused: + iconData = Icons.pause_circle_outline; + color = Colors.grey; + break; + case DownloadStatus.completed: + iconData = Icons.check_circle; + color = Colors.green; + break; + case DownloadStatus.failed: + iconData = Icons.error; + color = Colors.red; + break; + case DownloadStatus.cancelled: + iconData = Icons.cancel; + color = Colors.grey; + break; + case DownloadStatus.partial: + iconData = Icons.downloading; + color = Colors.orange; + break; + } + + return Icon(iconData, size: 20, color: color); + } + + /// Get summary text for container nodes (shows/seasons) + String _getNodeSummary(DownloadTreeNode node) { + final total = node.children.length; + final completed = node.completedChildrenCount; + return '$completed/$total completed'; + } + + /// Build action buttons for nodes + Widget _buildActions(DownloadTreeNode node) { + final globalKey = node.key; + final isContainer = + node.type == DownloadNodeType.show || + node.type == DownloadNodeType.season; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // For individual items (episodes/movies) + if (!isContainer) ...[ + // Pause button for downloading items + if (node.status == DownloadStatus.downloading && + widget.onPause != null) + IconButton( + icon: const Icon(Icons.pause, size: 20), + onPressed: () => widget.onPause!(globalKey), + tooltip: 'Pause', + ), + + // Resume button for paused items + if (node.status == DownloadStatus.paused && widget.onResume != null) + IconButton( + icon: const Icon(Icons.play_arrow, size: 20), + onPressed: () => widget.onResume!(globalKey), + tooltip: 'Resume', + ), + + // Cancel button for downloading/queued items + if ((node.status == DownloadStatus.downloading || + node.status == DownloadStatus.queued) && + widget.onCancel != null) + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => widget.onCancel!(globalKey), + tooltip: 'Cancel', + ), + + // Retry button for failed items + if (node.status == DownloadStatus.failed && widget.onRetry != null) + IconButton( + icon: const Icon(Icons.refresh, size: 20), + onPressed: () => widget.onRetry!(globalKey), + tooltip: t.downloads.retryDownload, + ), + + // Delete button for completed/failed/cancelled items + if ((node.status == DownloadStatus.completed || + node.status == DownloadStatus.failed || + node.status == DownloadStatus.cancelled) && + widget.onDelete != null) + IconButton( + icon: const Icon(Icons.delete, size: 20), + onPressed: () => widget.onDelete!(globalKey), + tooltip: 'Delete', + ), + ], + + // For container nodes (shows/seasons) + if (isContainer) ...[ + // Pause all button - show if any children are downloading or queued + if ((node.status == DownloadStatus.downloading || + node.status == DownloadStatus.queued) && + widget.onPause != null) + IconButton( + icon: const Icon(Icons.pause, size: 20), + onPressed: () => _pauseAllChildren(node), + tooltip: 'Pause all', + ), + + // Resume all button - show if container is paused + if (node.status == DownloadStatus.paused && widget.onResume != null) + IconButton( + icon: const Icon(Icons.play_arrow, size: 20), + onPressed: () => _resumeAllChildren(node), + tooltip: 'Resume all', + ), + + // Delete all button + if (widget.onDelete != null) + IconButton( + icon: const Icon(Icons.delete_sweep, size: 20), + onPressed: () => _deleteAllChildren(node), + tooltip: 'Delete all', + ), + ], + ], + ); + } + + /// Pause all active (downloading and queued) children of a container node + void _pauseAllChildren(DownloadTreeNode node) { + final keys = _getActiveChildKeys(node); + for (final key in keys) { + widget.onPause?.call(key); + } + } + + /// Resume all paused children of a container node + void _resumeAllChildren(DownloadTreeNode node) { + final keys = _getPausedChildKeys(node); + for (final key in keys) { + widget.onResume?.call(key); + } + } + + /// Get all active (downloading or queued) child keys from a container node + List _getActiveChildKeys(DownloadTreeNode node) { + final List keys = []; + for (final child in node.children) { + if (child.hasChildren) { + keys.addAll(_getActiveChildKeys(child)); + } else if (child.status == DownloadStatus.downloading || + child.status == DownloadStatus.queued) { + keys.add(child.key); + } + } + return keys; + } + + /// Get all paused child keys from a container node + List _getPausedChildKeys(DownloadTreeNode node) { + final List keys = []; + for (final child in node.children) { + if (child.hasChildren) { + keys.addAll(_getPausedChildKeys(child)); + } else if (child.status == DownloadStatus.paused) { + keys.add(child.key); + } + } + return keys; + } + + /// Delete all children of a container node + void _deleteAllChildren(DownloadTreeNode node) { + final allKeys = _getAllChildKeys(node); + for (final key in allKeys) { + widget.onDelete?.call(key); + } + } + + /// Get all leaf node keys from a container node + List _getAllChildKeys(DownloadTreeNode node) { + final List keys = []; + + for (final child in node.children) { + if (child.hasChildren) { + keys.addAll(_getAllChildKeys(child)); + } else { + keys.add(child.key); + } + } + + return keys; + } +} + +/// Helper class to store a node with its depth in the flattened tree +class _FlatNode { + final DownloadTreeNode node; + final int depth; + + const _FlatNode({required this.node, required this.depth}); +} diff --git a/lib/widgets/focusable_media_card.dart b/lib/widgets/focusable_media_card.dart index 3fd65526..317a2c6b 100644 --- a/lib/widgets/focusable_media_card.dart +++ b/lib/widgets/focusable_media_card.dart @@ -20,6 +20,9 @@ class FocusableMediaCard extends StatefulWidget { final bool isInContinueWatching; final String? collectionId; + /// True for downloaded content without server access + final bool isOffline; + /// Optional external focus node for programmatic focus control. /// If not provided, an internal focus node is created. final FocusNode? focusNode; @@ -43,6 +46,7 @@ class FocusableMediaCard extends StatefulWidget { this.forceGridMode = false, this.isInContinueWatching = false, this.collectionId, + this.isOffline = false, this.focusNode, this.onNavigateUp, this.onBack, @@ -78,6 +82,7 @@ class _FocusableMediaCardState extends State { forceGridMode: widget.forceGridMode, isInContinueWatching: widget.isInContinueWatching, collectionId: widget.collectionId, + isOffline: widget.isOffline, ), ); } diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index be8e4fea..816a31a0 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -227,10 +227,7 @@ class HubSectionState extends State { final Map> _mediaCardKeys = {}; GlobalKey _getMediaCardKey(int index) { - return _mediaCardKeys.putIfAbsent( - index, - () => GlobalKey(), - ); + return _mediaCardKeys.putIfAbsent(index, () => GlobalKey()); } void _activateCurrentItem() { @@ -346,8 +343,9 @@ class HubSectionState extends State { child: _LockedHubItemWrapper( isFocused: isItemFocused, onTap: () => _onItemTapped(index), - onLongPress: () => - _mediaCardKeys[index]?.currentState?.showContextMenu(), + onLongPress: () => _mediaCardKeys[index] + ?.currentState + ?.showContextMenu(), child: MediaCard( key: _getMediaCardKey(index), item: item, diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index de1114a3..0e4c34fb 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -3,7 +3,9 @@ import 'package:provider/provider.dart'; import '../../services/plex_client.dart'; import '../models/plex_metadata.dart'; import '../models/plex_playlist.dart'; +import '../providers/download_provider.dart'; import '../providers/multi_server_provider.dart'; +import '../services/download_storage_service.dart'; import '../providers/settings_provider.dart'; import '../services/settings_service.dart'; import '../utils/provider_extensions.dart'; @@ -31,6 +33,7 @@ class MediaCard extends StatefulWidget { final bool isInContinueWatching; final String? collectionId; // The collection ID if displaying within a collection + final bool isOffline; // True for downloaded content without server access const MediaCard({ super.key, @@ -43,6 +46,7 @@ class MediaCard extends StatefulWidget { this.forceGridMode = false, this.isInContinueWatching = false, this.collectionId, + this.isOffline = false, }); @override @@ -164,6 +168,7 @@ class MediaCardState extends State { final result = await navigateToVideoPlayer( context, metadata: widget.item, + isOffline: widget.isOffline, ); // Refresh parent screen if result indicates it's needed if (result == true) { @@ -184,7 +189,10 @@ class MediaCardState extends State { final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => MediaDetailScreen(metadata: widget.item), + builder: (context) => MediaDetailScreen( + metadata: widget.item, + isOffline: widget.isOffline, + ), ), ); // Refresh parent screen if result indicates it's needed @@ -194,6 +202,25 @@ class MediaCardState extends State { } } + /// Get the local poster path for offline mode + String? _getLocalPosterPath(BuildContext context) { + if (!widget.isOffline) return null; + if (widget.item is! PlexMetadata) return null; + + final metadata = widget.item as PlexMetadata; + if (metadata.serverId == null) return null; + + final downloadProvider = context.read(); + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + + // Get artwork reference and resolve to local path using hash (includes serverId) + final artwork = downloadProvider.getArtworkPaths(globalKey); + return artwork?.getLocalPath( + DownloadStorageService.instance, + metadata.serverId!, + ); + } + @override Widget build(BuildContext context) { final settingsProvider = context.watch(); @@ -202,6 +229,7 @@ class MediaCardState extends State { : settingsProvider.viewMode; final semanticLabel = _buildSemanticLabel(); + final localPosterPath = _getLocalPosterPath(context); final cardWidget = viewMode == ViewMode.grid ? _MediaCardGrid( @@ -211,6 +239,8 @@ class MediaCardState extends State { semanticLabel: semanticLabel, onTap: () => _handleTap(context), onLongPress: _showContextMenu, + isOffline: widget.isOffline, + localPosterPath: localPosterPath, ) : _MediaCardList( item: widget.item, @@ -218,6 +248,8 @@ class MediaCardState extends State { onTap: () => _handleTap(context), onLongPress: _showContextMenu, density: settingsProvider.libraryDensity, + isOffline: widget.isOffline, + localPosterPath: localPosterPath, ); // Use context menu for both PlexMetadata and PlexPlaylist items @@ -243,6 +275,8 @@ class _MediaCardGrid extends StatelessWidget { final String semanticLabel; final VoidCallback onTap; final VoidCallback onLongPress; + final bool isOffline; + final String? localPosterPath; const _MediaCardGrid({ required this.item, @@ -251,6 +285,8 @@ class _MediaCardGrid extends StatelessWidget { required this.semanticLabel, required this.onTap, required this.onLongPress, + this.isOffline = false, + this.localPosterPath, }); @override @@ -321,7 +357,12 @@ class _MediaCardGrid extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(8), - child: _buildPosterImage(context, item), + child: _buildPosterImage( + context, + item, + isOffline: isOffline, + localPosterPath: localPosterPath, + ), ), _PosterOverlay(item: item), ], @@ -336,6 +377,8 @@ class _MediaCardList extends StatelessWidget { final VoidCallback onTap; final VoidCallback onLongPress; final LibraryDensity density; + final bool isOffline; + final String? localPosterPath; const _MediaCardList({ required this.item, @@ -343,6 +386,8 @@ class _MediaCardList extends StatelessWidget { required this.onTap, required this.onLongPress, required this.density, + this.isOffline = false, + this.localPosterPath, }); double get _posterWidth { @@ -522,7 +567,12 @@ class _MediaCardList extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(8), - child: _buildPosterImage(context, item), + child: _buildPosterImage( + context, + item, + isOffline: isOffline, + localPosterPath: localPosterPath, + ), ), _PosterOverlay(item: item), ], @@ -629,7 +679,12 @@ PlexClient _getClientForItem(BuildContext context, dynamic item) { return context.getClientForServer(serverId); } -Widget _buildPosterImage(BuildContext context, dynamic item) { +Widget _buildPosterImage( + BuildContext context, + dynamic item, { + bool isOffline = false, + String? localPosterPath, +}) { String? posterUrl; IconData fallbackIcon = Icons.movie; @@ -638,22 +693,24 @@ Widget _buildPosterImage(BuildContext context, dynamic item) { fallbackIcon = Icons.playlist_play; return PlexOptimizedImage.playlist( - client: _getClientForItem(context, item), + client: isOffline ? null : _getClientForItem(context, item), imagePath: posterUrl, width: double.infinity, height: double.infinity, fit: BoxFit.cover, + localFilePath: localPosterPath, ); } else if (item is PlexMetadata) { final useSeasonPoster = context.watch().useSeasonPoster; posterUrl = item.posterThumb(useSeasonPoster: useSeasonPoster); return PlexOptimizedImage.poster( - client: _getClientForItem(context, item), + client: isOffline ? null : _getClientForItem(context, item), imagePath: posterUrl, width: double.infinity, height: double.infinity, fit: BoxFit.cover, + localFilePath: localPosterPath, ); } diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 1e6c6075..bdb9feb9 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -6,12 +6,16 @@ import '../services/play_queue_launcher.dart'; import '../models/plex_metadata.dart'; import '../models/plex_playlist.dart'; import '../providers/multi_server_provider.dart'; +import '../providers/download_provider.dart'; +import '../providers/offline_mode_provider.dart'; +import '../providers/offline_watch_provider.dart'; import '../utils/provider_extensions.dart'; import '../utils/app_logger.dart'; import '../utils/collection_playlist_play_helper.dart'; import '../utils/library_refresh_notifier.dart'; import '../screens/media_detail_screen.dart'; import '../screens/season_detail_screen.dart'; +import '../utils/smart_deletion_handler.dart'; import '../widgets/file_info_bottom_sheet.dart'; import '../widgets/focusable_bottom_sheet.dart'; import '../widgets/focusable_list_tile.dart'; @@ -264,6 +268,39 @@ class MediaContextMenuState extends State { ); } + // Download options (for episodes, movies, shows, and seasons) + if (itemType == 'episode' || + itemType == 'movie' || + itemType == 'show' || + itemType == 'season') { + final downloadProvider = Provider.of( + context, + listen: false, + ); + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + final isDownloaded = downloadProvider.isDownloaded(globalKey); + + if (isDownloaded) { + // Show delete download option + menuActions.add( + _MenuAction( + value: 'delete_download', + icon: Icons.delete, + label: t.downloads.deleteDownload, + ), + ); + } else { + // Show download option + menuActions.add( + _MenuAction( + value: 'download', + icon: Icons.download, + label: t.downloads.downloadNow, + ), + ); + } + } + // Add to... (for episodes, movies, shows, and seasons) if (itemType == 'episode' || itemType == 'movie' || @@ -320,21 +357,55 @@ class MediaContextMenuState extends State { if (!context.mounted) return; + // Check if we're in offline mode for watch actions + final offlineModeProvider = context.read(); + final isOffline = offlineModeProvider.isOffline; + switch (selected) { case 'watch': - await _executeAction( - context, - () => client.markAsWatched(metadata!.ratingKey), - t.messages.markedAsWatched, - ); + if (isOffline && metadata?.serverId != null) { + // Offline mode: queue action for later sync + final offlineWatch = context.read(); + await offlineWatch.markAsWatched( + serverId: metadata!.serverId!, + ratingKey: metadata.ratingKey, + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.markedAsWatchedOffline)), + ); + widget.onRefresh?.call(metadata.ratingKey); + } + } else { + await _executeAction( + context, + () => client.markAsWatched(metadata!.ratingKey), + t.messages.markedAsWatched, + ); + } break; case 'unwatch': - await _executeAction( - context, - () => client.markAsUnwatched(metadata!.ratingKey), - t.messages.markedAsUnwatched, - ); + if (isOffline && metadata?.serverId != null) { + // Offline mode: queue action for later sync + final offlineWatch = context.read(); + await offlineWatch.markAsUnwatched( + serverId: metadata!.serverId!, + ratingKey: metadata.ratingKey, + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.markedAsUnwatchedOffline)), + ); + widget.onRefresh?.call(metadata.ratingKey); + } + } else { + await _executeAction( + context, + () => client.markAsUnwatched(metadata!.ratingKey), + t.messages.markedAsUnwatched, + ); + } break; case 'remove_from_continue_watching': @@ -410,6 +481,14 @@ class MediaContextMenuState extends State { case 'delete': await _handleDelete(context, isCollection, isPlaylist); break; + + case 'download': + await _handleDownload(context); + break; + + case 'delete_download': + await _handleDeleteDownload(context); + break; } } finally { _isContextMenuOpen = false; @@ -451,7 +530,7 @@ class MediaContextMenuState extends State { final client = _getClientForItem(); try { - final metadata = await client.getMetadata(ratingKey); + final metadata = await client.getMetadataWithImages(ratingKey); if (metadata != null && context.mounted) { await Navigator.push( context, @@ -750,7 +829,9 @@ class MediaContextMenuState extends State { if (sectionId == null) { try { appLogger.d(' - Fetching full metadata for: ${metadata.ratingKey}'); - final fullMetadata = await client.getMetadata(metadata.ratingKey); + final fullMetadata = await client.getMetadataWithImages( + metadata.ratingKey, + ); if (fullMetadata != null) { sectionId = fullMetadata.librarySectionID; appLogger.d(' - Section ID from full metadata: $sectionId'); @@ -777,7 +858,7 @@ class MediaContextMenuState extends State { appLogger.d( ' - Trying to get from parent: ${metadata.grandparentRatingKey}', ); - final parentMeta = await client.getMetadata( + final parentMeta = await client.getMetadataWithImages( metadata.grandparentRatingKey!, ); sectionId = parentMeta?.librarySectionID; @@ -1149,6 +1230,98 @@ class MediaContextMenuState extends State { } } + /// Handle download action + Future _handleDownload(BuildContext context) async { + final downloadProvider = Provider.of( + context, + listen: false, + ); + final metadata = widget.item as PlexMetadata; + final client = _getClientForItem(); + + try { + final count = await downloadProvider.queueDownload(metadata, client); + if (context.mounted) { + // Show appropriate message based on count + final message = count > 1 + ? t.downloads.episodesQueued(count: count) + : t.downloads.downloadQueued; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + } on CellularDownloadBlockedException { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.settings.cellularDownloadBlocked)), + ); + } + } catch (e) { + appLogger.e('Failed to queue download', error: e); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))), + ); + } + } + } + + /// Handle delete download action + Future _handleDeleteDownload(BuildContext context) async { + final downloadProvider = Provider.of( + context, + listen: false, + ); + final metadata = widget.item as PlexMetadata; + final globalKey = '${metadata.serverId}:${metadata.ratingKey}'; + + // Show confirmation dialog + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.downloads.deleteDownload), + content: Text(t.downloads.deleteConfirm(title: metadata.title)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(t.common.cancel), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: Text(t.common.delete), + ), + ], + ), + ); + + if (confirmed != true || !context.mounted) return; + + try { + // Use smart deletion handler (shows progress only if >500ms) + await SmartDeletionHandler.deleteWithProgress( + context: context, + provider: downloadProvider, + globalKey: globalKey, + ); + + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(t.downloads.downloadDeleted))); + // Refresh the view if needed + widget.onRefresh?.call(metadata.ratingKey); + } + } catch (e) { + appLogger.e('Failed to delete download', error: e); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))), + ); + } + } + } + @override Widget build(BuildContext context) { return GestureDetector( diff --git a/lib/widgets/plex_optimized_image.dart b/lib/widgets/plex_optimized_image.dart index 3cdbad7e..72b854cb 100644 --- a/lib/widgets/plex_optimized_image.dart +++ b/lib/widgets/plex_optimized_image.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../services/plex_client.dart'; @@ -5,7 +6,7 @@ import '../utils/plex_image_helper.dart'; import 'media_card.dart'; class PlexOptimizedImage extends StatelessWidget { - final PlexClient client; + final PlexClient? client; final String? imagePath; final double? width; final double? height; @@ -19,10 +20,11 @@ class PlexOptimizedImage extends StatelessWidget { final Alignment alignment; final IconData? fallbackIcon; final ImageType imageType; + final String? localFilePath; const PlexOptimizedImage._({ super.key, - required this.client, + this.client, required this.imagePath, this.width, this.height, @@ -36,12 +38,13 @@ class PlexOptimizedImage extends StatelessWidget { this.alignment = Alignment.center, this.fallbackIcon, this.imageType = ImageType.poster, + this.localFilePath, }); /// Generic constructor for optimized images. const factory PlexOptimizedImage({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -55,12 +58,13 @@ class PlexOptimizedImage extends StatelessWidget { Alignment alignment, IconData? fallbackIcon, ImageType imageType, + String? localFilePath, }) = PlexOptimizedImage._; /// Named constructor for poster images with default fallback icon. const factory PlexOptimizedImage.poster({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -72,12 +76,13 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding, String? cacheKey, Alignment alignment, + String? localFilePath, }) = PlexOptimizedImage._poster; /// Named constructor for episode thumbnails. const factory PlexOptimizedImage.thumb({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -89,12 +94,13 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding, String? cacheKey, Alignment alignment, + String? localFilePath, }) = PlexOptimizedImage._thumb; /// Named constructor for playlist images. const factory PlexOptimizedImage.playlist({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -106,11 +112,12 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding, String? cacheKey, Alignment alignment, + String? localFilePath, }) = PlexOptimizedImage._playlist; const PlexOptimizedImage._poster({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -122,6 +129,7 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding = true, String? cacheKey, Alignment alignment = Alignment.center, + String? localFilePath, }) : this._( key: key, client: client, @@ -138,11 +146,12 @@ class PlexOptimizedImage extends StatelessWidget { alignment: alignment, fallbackIcon: Icons.movie, imageType: ImageType.poster, + localFilePath: localFilePath, ); const PlexOptimizedImage._thumb({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -154,6 +163,7 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding = true, String? cacheKey, Alignment alignment = Alignment.center, + String? localFilePath, }) : this._( key: key, client: client, @@ -170,11 +180,12 @@ class PlexOptimizedImage extends StatelessWidget { alignment: alignment, fallbackIcon: Icons.video_library, imageType: ImageType.thumb, + localFilePath: localFilePath, ); const PlexOptimizedImage._playlist({ Key? key, - required PlexClient client, + PlexClient? client, required String? imagePath, double? width, double? height, @@ -186,6 +197,7 @@ class PlexOptimizedImage extends StatelessWidget { bool enableTranscoding = true, String? cacheKey, Alignment alignment = Alignment.center, + String? localFilePath, }) : this._( key: key, client: client, @@ -202,10 +214,28 @@ class PlexOptimizedImage extends StatelessWidget { alignment: alignment, fallbackIcon: Icons.playlist_play, imageType: ImageType.poster, + localFilePath: localFilePath, ); @override Widget build(BuildContext context) { + // Check for local file first + if (localFilePath != null) { + final file = File(localFilePath!); + if (file.existsSync()) { + return Image.file( + file, + width: width, + height: height, + fit: fit, + filterQuality: filterQuality, + alignment: alignment, + errorBuilder: (context, error, stackTrace) => + _buildErrorWidget(context, error), + ); + } + } + double resolvedDimension( double? explicit, double constraintMax, diff --git a/lib/widgets/side_navigation_rail.dart b/lib/widgets/side_navigation_rail.dart index ea15e795..d90b3d3e 100644 --- a/lib/widgets/side_navigation_rail.dart +++ b/lib/widgets/side_navigation_rail.dart @@ -70,7 +70,9 @@ class NavigationRailItem extends StatelessWidget { : isSelected ? t.text.withValues(alpha: 0.1) // Just selected : isFocused - ? t.text.withValues(alpha: 0.12) // Just focused (more visible) + ? t.text.withValues( + alpha: 0.12, + ) // Just focused (more visible) : null, borderRadius: borderRadius, ), @@ -96,6 +98,7 @@ class NavigationRailItem extends StatelessWidget { class SideNavigationRail extends StatefulWidget { final int selectedIndex; final String? selectedLibraryKey; + final bool isOfflineMode; final ValueChanged onDestinationSelected; final ValueChanged onLibrarySelected; @@ -103,6 +106,7 @@ class SideNavigationRail extends StatefulWidget { super.key, required this.selectedIndex, this.selectedLibraryKey, + this.isOfflineMode = false, required this.onDestinationSelected, required this.onLibrarySelected, }); @@ -120,12 +124,14 @@ class SideNavigationRailState extends State { late FocusNode _homeFocusNode; late FocusNode _librariesFocusNode; late FocusNode _searchFocusNode; + late FocusNode _downloadsFocusNode; late FocusNode _settingsFocusNode; // Focus state tracking bool _isHomeFocused = false; bool _isLibrariesFocused = false; bool _isSearchFocused = false; + bool _isDownloadsFocused = false; bool _isSettingsFocused = false; // Map to store library item focus nodes and states @@ -138,6 +144,7 @@ class SideNavigationRailState extends State { _homeFocusNode = FocusNode(debugLabel: 'nav_home'); _librariesFocusNode = FocusNode(debugLabel: 'nav_libraries'); _searchFocusNode = FocusNode(debugLabel: 'nav_search'); + _downloadsFocusNode = FocusNode(debugLabel: 'nav_downloads'); _settingsFocusNode = FocusNode(debugLabel: 'nav_settings'); _homeFocusNode.addListener( @@ -155,6 +162,11 @@ class SideNavigationRailState extends State { setState(() => _isSearchFocused = _searchFocusNode.hasFocus); }), ); + _downloadsFocusNode.addListener( + () => _onFocusChange(_downloadsFocusNode, () { + setState(() => _isDownloadsFocused = _downloadsFocusNode.hasFocus); + }), + ); _settingsFocusNode.addListener( () => _onFocusChange(_settingsFocusNode, () { setState(() => _isSettingsFocused = _settingsFocusNode.hasFocus); @@ -173,6 +185,7 @@ class SideNavigationRailState extends State { _homeFocusNode.dispose(); _librariesFocusNode.dispose(); _searchFocusNode.dispose(); + _downloadsFocusNode.dispose(); _settingsFocusNode.dispose(); for (final node in _libraryFocusNodes.values) { node.dispose(); @@ -218,6 +231,9 @@ class SideNavigationRailState extends State { _searchFocusNode.requestFocus(); break; case 3: + _downloadsFocusNode.requestFocus(); + break; + case 4: _settingsFocusNode.requestFocus(); break; } @@ -359,45 +375,69 @@ class SideNavigationRailState extends State { child: ListView( padding: const EdgeInsets.symmetric(horizontal: 12), children: [ - // Home + // In offline mode, only show Downloads and Settings + if (!widget.isOfflineMode) ...[ + // Home + _buildNavItem( + icon: Icons.home_outlined, + selectedIcon: Icons.home, + label: Translations.of(context).navigation.home, + isSelected: widget.selectedIndex == 0, + isFocused: _isHomeFocused, + onTap: () => widget.onDestinationSelected(0), + focusNode: _homeFocusNode, + ), + + const SizedBox(height: 8), + + // Libraries section + _buildLibrariesSection(visibleLibraries, t), + + const SizedBox(height: 8), + + // Search + _buildNavItem( + icon: Icons.search, + selectedIcon: Icons.search, + label: Translations.of(context).navigation.search, + isSelected: widget.selectedIndex == 2, + isFocused: _isSearchFocused, + onTap: () => widget.onDestinationSelected(2), + focusNode: _searchFocusNode, + ), + + const SizedBox(height: 8), + ], + + // Downloads (index 0 in offline mode, 3 in online mode) _buildNavItem( - icon: Icons.home_outlined, - selectedIcon: Icons.home, - label: Translations.of(context).navigation.home, - isSelected: widget.selectedIndex == 0, - isFocused: _isHomeFocused, - onTap: () => widget.onDestinationSelected(0), - focusNode: _homeFocusNode, + icon: Icons.download_outlined, + selectedIcon: Icons.download, + label: Translations.of(context).navigation.downloads, + isSelected: widget.isOfflineMode + ? widget.selectedIndex == 0 + : widget.selectedIndex == 3, + isFocused: _isDownloadsFocused, + onTap: () => widget.onDestinationSelected( + widget.isOfflineMode ? 0 : 3, + ), + focusNode: _downloadsFocusNode, ), const SizedBox(height: 8), - // Libraries section - _buildLibrariesSection(visibleLibraries, t), - - const SizedBox(height: 8), - - // Search - _buildNavItem( - icon: Icons.search, - selectedIcon: Icons.search, - label: Translations.of(context).navigation.search, - isSelected: widget.selectedIndex == 2, - isFocused: _isSearchFocused, - onTap: () => widget.onDestinationSelected(2), - focusNode: _searchFocusNode, - ), - - const SizedBox(height: 8), - - // Settings + // Settings (index 1 in offline mode, 4 in online mode) _buildNavItem( icon: Icons.settings_outlined, selectedIcon: Icons.settings, label: Translations.of(context).navigation.settings, - isSelected: widget.selectedIndex == 3, + isSelected: widget.isOfflineMode + ? widget.selectedIndex == 1 + : widget.selectedIndex == 4, isFocused: _isSettingsFocused, - onTap: () => widget.onDestinationSelected(3), + onTap: () => widget.onDestinationSelected( + widget.isOfflineMode ? 1 : 4, + ), focusNode: _settingsFocusNode, ), ], diff --git a/lib/widgets/video_controls/sheets/chapter_sheet.dart b/lib/widgets/video_controls/sheets/chapter_sheet.dart index a72494d5..34ec8ef7 100644 --- a/lib/widgets/video_controls/sheets/chapter_sheet.dart +++ b/lib/widgets/video_controls/sheets/chapter_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../mpv/mpv.dart'; import '../../../services/plex_client.dart'; +import '../../../services/download_storage_service.dart'; import '../../../models/plex_media_info.dart'; import '../../../utils/duration_formatter.dart'; import '../../../utils/provider_extensions.dart'; @@ -67,9 +68,14 @@ class _ChapterSheetState extends State { super.dispose(); } - /// Get the correct PlexClient for the metadata's server - PlexClient _getClientForChapters(BuildContext context) { - return context.getClientForServer(widget.serverId!); + /// Get the PlexClient for chapters, or null if unavailable (offline mode) + PlexClient? _tryGetClientForChapters(BuildContext context) { + if (widget.serverId == null) return null; + try { + return context.getClientForServer(widget.serverId!); + } catch (_) { + return null; + } } @override @@ -117,6 +123,15 @@ class _ChapterSheetState extends State { final chapter = widget.chapters[index]; final isCurrentChapter = currentChapterIndex == index; + // Get local file path for offline chapter thumbnails + final localThumbPath = + widget.serverId != null && chapter.thumb != null + ? DownloadStorageService.instance.getArtworkPathSync( + widget.serverId!, + chapter.thumb!, + ) + : null; + return FocusableListTile( focusNode: index == 0 ? _initialFocusNode : null, leading: chapter.thumb != null @@ -125,8 +140,9 @@ class _ChapterSheetState extends State { ClipRRect( borderRadius: BorderRadius.circular(4), child: PlexOptimizedImage.thumb( - client: _getClientForChapters(context), + client: _tryGetClientForChapters(context), imagePath: chapter.thumb, + localFilePath: localThumbPath, width: 60, height: 34, fit: BoxFit.cover, diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index dd665ed0..ae2045ef 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -16,6 +16,7 @@ import 'package:window_manager/window_manager.dart'; import '../../mpv/mpv.dart'; import '../../services/plex_client.dart'; +import '../../services/plex_api_cache.dart'; import '../../models/plex_media_info.dart'; import '../../models/plex_media_version.dart'; import '../../models/plex_metadata.dart'; @@ -26,6 +27,7 @@ import '../../utils/platform_detector.dart'; import '../../utils/player_utils.dart'; import '../../utils/provider_extensions.dart'; import '../../utils/video_control_icons.dart'; +import '../../utils/app_logger.dart'; import '../../i18n/strings.g.dart'; import '../../focus/input_mode_tracker.dart'; import 'widgets/track_chapter_controls.dart'; @@ -148,8 +150,7 @@ class _PlexVideoControlsState extends State leading: true, trailing: true, ); - _loadChapters(); - _loadMarkers(); + _loadPlaybackExtras(); _loadSeekTimes(); _startHideTimer(); _initKeyboardService(); @@ -562,29 +563,103 @@ class _PlexVideoControlsState extends State } } - Future _loadChapters() async { - final client = _getClientForMetadata(); + Future _loadPlaybackExtras() async { + try { + appLogger.d( + '_loadPlaybackExtras: starting for ${widget.metadata.ratingKey}', + ); + final client = _getClientForMetadata(); + appLogger.d( + '_loadPlaybackExtras: got client with serverId=${client.serverId}', + ); - final chapters = await client.getChapters(widget.metadata.ratingKey); - if (mounted) { - setState(() { - _chapters = chapters; - _chaptersLoaded = true; - }); + final extras = await client.getPlaybackExtras(widget.metadata.ratingKey); + appLogger.d( + '_loadPlaybackExtras: got ${extras.chapters.length} chapters', + ); + + if (mounted) { + setState(() { + _chapters = extras.chapters; + _markers = extras.markers; + _chaptersLoaded = true; + _markersLoaded = true; + }); + } + } catch (e, stack) { + // Fallback: try to load from cache directly (for offline playback) + appLogger.d( + '_loadPlaybackExtras: client unavailable, trying cache fallback', + ); + final serverId = widget.metadata.serverId; + if (serverId != null) { + final cacheKey = '/library/metadata/${widget.metadata.ratingKey}'; + final cached = await PlexApiCache.instance.get(serverId, cacheKey); + if (cached != null) { + final extras = _parsePlaybackExtrasFromCache(cached); + appLogger.d( + '_loadPlaybackExtras: loaded ${extras.chapters.length} chapters from cache', + ); + if (mounted) { + setState(() { + _chapters = extras.chapters; + _markers = extras.markers; + _chaptersLoaded = true; + _markersLoaded = true; + }); + } + return; + } + } + appLogger.e('_loadPlaybackExtras failed', error: e, stackTrace: stack); } } - Future _loadMarkers() async { - final client = _getClientForMetadata(); + /// Parse PlaybackExtras from cached API response (for offline playback) + PlaybackExtras _parsePlaybackExtrasFromCache(Map cached) { + final chapters = []; + final markers = []; - final markers = await client.getMarkers(widget.metadata.ratingKey); - - if (mounted) { - setState(() { - _markers = markers; - _markersLoaded = true; - }); + Map? metadataJson; + if (cached['MediaContainer']?['Metadata'] != null && + (cached['MediaContainer']['Metadata'] as List).isNotEmpty) { + metadataJson = + cached['MediaContainer']['Metadata'][0] as Map; } + + if (metadataJson != null) { + // Parse chapters + if (metadataJson['Chapter'] != null) { + for (var chapter in metadataJson['Chapter'] as List) { + chapters.add( + PlexChapter( + id: chapter['id'] as int, + index: chapter['index'] as int?, + startTimeOffset: chapter['startTimeOffset'] as int?, + endTimeOffset: chapter['endTimeOffset'] as int?, + title: chapter['tag'] as String?, + thumb: chapter['thumb'] as String?, + ), + ); + } + } + + // Parse markers + if (metadataJson['Marker'] != null) { + for (var marker in metadataJson['Marker'] as List) { + markers.add( + PlexMarker( + id: marker['id'] as int, + type: marker['type'] as String, + startTimeOffset: marker['startTimeOffset'] as int, + endTimeOffset: marker['endTimeOffset'] as int, + ), + ); + } + } + } + + return PlaybackExtras(chapters: chapters, markers: markers); } Widget _buildTrackChapterControlsWidget() { @@ -830,7 +905,8 @@ class _PlexVideoControlsState extends State @override Widget build(BuildContext context) { // Use desktop controls for desktop platforms AND Android TV - final isMobile = PlatformDetector.isMobile(context) && !PlatformDetector.isTV(); + final isMobile = + PlatformDetector.isMobile(context) && !PlatformDetector.isTV(); return Focus( focusNode: _focusNode, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index da1af78e..25bbda5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 46571722..11f8fb02 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST hotkey_manager_linux os_media_controls screen_retriever_linux + sqlite3_flutter_libs url_launcher_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ef0e2808..245595b6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import connectivity_plus import device_info_plus +import file_picker import gamepads_darwin import hotkey_manager_macos import macos_window_utils @@ -16,12 +17,15 @@ import path_provider_foundation import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin +import sqlite3_flutter_libs import url_launcher_macos +import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) @@ -31,6 +35,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d5529a0b..9c0801c5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,8 @@ PODS: - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - gamepads_darwin (0.1.1): - FlutterMacOS @@ -27,14 +29,42 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - sqlite3 (3.51.1): + - sqlite3/common (= 3.51.1) + - sqlite3/common (3.51.1) + - sqlite3/dbstatvtab (3.51.1): + - sqlite3/common + - sqlite3/fts5 (3.51.1): + - sqlite3/common + - sqlite3/math (3.51.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.51.1): + - sqlite3/common + - sqlite3/rtree (3.51.1): + - sqlite3/common + - sqlite3/session (3.51.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.51.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - sqlite3/session - url_launcher_macos (0.0.1): - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) @@ -45,18 +75,23 @@ DEPENDENCIES: - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: - HotKey + - sqlite3 EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos FlutterMacOS: :path: Flutter/ephemeral gamepads_darwin: @@ -77,14 +112,19 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 gamepads_darwin: 643b6a69e20ca678fae83781b7f7fc8f15f5d710 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 @@ -96,7 +136,10 @@ SPEC CHECKSUMS: screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b + sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 2a199ece..5d40372b 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = Plezy // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1 +PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 edde746 diff --git a/pubspec.lock b/pubspec.lock index 643f5655..f1207683 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -225,8 +233,16 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - crypto: + cross_file: dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" + crypto: + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -305,6 +321,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1" + url: "https://pub.dev" + source: hosted + version: "2.28.2" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4" + url: "https://pub.dev" + source: hosted + version: "2.28.0" duration: dependency: "direct main" description: @@ -337,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" fixnum: dependency: transitive description: @@ -390,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" flutter_test: dependency: "direct dev" description: flutter @@ -921,6 +969,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" rxdart: dependency: transitive description: @@ -929,6 +985,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + saf_stream: + dependency: "direct main" + description: + name: saf_stream + sha256: c05449997698c481a03e428162a999f93b1ee1bcc0349d651899a59f7b10230a + url: "https://pub.dev" + source: hosted + version: "0.12.3" + saf_util: + dependency: "direct main" + description: + name: saf_util + sha256: "219f983e5f17b28998335158cdc97add9d52af9884e38b5a43f10dcc070510ec" + url: "https://pub.dev" + source: hosted + version: "0.11.0" screen_retriever: dependency: transitive description: @@ -1134,6 +1206,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" + url: "https://pub.dev" + source: hosted + version: "2.9.4" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "1e800ebe7f85a80a66adacaa6febe4d5f4d8b75f244e9838a27cb2ffc7aec08d" + url: "https://pub.dev" + source: hosted + version: "0.5.41" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67" + url: "https://pub.dev" + source: hosted + version: "0.41.2" stack_trace: dependency: transitive description: @@ -1374,6 +1470,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.3" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" + url: "https://pub.dev" + source: hosted + version: "0.9.0+3" + workmanager_android: + dependency: transitive + description: + name: workmanager_android + sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" + url: "https://pub.dev" + source: hosted + version: "0.9.0+2" + workmanager_apple: + dependency: transitive + description: + name: workmanager_apple + sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" + url: "https://pub.dev" + source: hosted + version: "0.9.1+2" + workmanager_platform_interface: + dependency: transitive + description: + name: workmanager_platform_interface + sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 + url: "https://pub.dev" + source: hosted + version: "0.9.1+1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ff40ebf5..73c8ddfb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,13 @@ dependencies: path_provider: ^2.1.0 path: ^1.9.0 gamepads: ^0.1.9 + drift: ^2.28.2 + sqlite3_flutter_libs: ^0.5.41 + workmanager: ^0.9.0+3 + crypto: ^3.0.7 + file_picker: ^8.0.0 + saf_util: ^0.11.0 + saf_stream: ^0.12.2 dev_dependencies: flutter_test: @@ -47,6 +54,7 @@ dev_dependencies: flutter_launcher_icons: ^0.14.4 slang_build_runner: ^3.31.0 dart_code_linter: ^3.1.1 + drift_dev: ^2.14.0 flutter: uses-material-design: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6dfc4b49..6f4f5bcb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b4a0397f..bcb19105 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST hotkey_manager_windows os_media_controls screen_retriever_windows + sqlite3_flutter_libs url_launcher_windows window_manager ) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 6947dcae..265245f9 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,11 +89,11 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" + VALUE "CompanyName", "com.edde746" "\0" VALUE "FileDescription", "Plezy" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "Plezy" "\0" - VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 edde746. All rights reserved." "\0" VALUE "OriginalFilename", "Plezy.exe" "\0" VALUE "ProductName", "Plezy" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"