From b0c5255bd7196d088e0bdfc62928d08a6e992de6 Mon Sep 17 00:00:00 2001 From: tkymmm <136296842+tkymmm@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:09:36 +0900 Subject: [PATCH 01/93] feat: add japanese translations (#552) --- i18n.ts | 9 +- translations/ja.json | 457 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 translations/ja.json diff --git a/i18n.ts b/i18n.ts index ff8cc4e5..99ba26a7 100644 --- a/i18n.ts +++ b/i18n.ts @@ -5,9 +5,10 @@ import de from "./translations/de.json"; import en from "./translations/en.json"; import es from "./translations/es.json"; import fr from "./translations/fr.json"; +import it from "./translations/it.json"; +import ja from "./translations/ja.json"; import nl from "./translations/nl.json"; import sv from "./translations/sv.json"; -import it from "./translations/it.json"; import zhTW from './translations/zh-TW.json'; import { getLocales } from "expo-localization"; @@ -16,9 +17,10 @@ export const APP_LANGUAGES = [ { label: "English", value: "en" }, { label: "Español", value: "es" }, { label: "Français", value: "fr" }, + { label: "Italiano", value: "it" }, + { label: "日本語", value: "ja" }, { label: "Nederlands", value: "nl" }, { label: "Svenska", value: "sv" }, - { label: "Italiano", value: "it" }, { label: "繁體中文", value: "zh-TW" }, ]; @@ -29,9 +31,10 @@ i18n.use(initReactI18next).init({ en: { translation: en }, es: { translation: es }, fr: { translation: fr }, + it: { translation: it }, + ja: { translation: ja }, nl: { translation: nl }, sv: { translation: sv }, - it: { translation: it }, "zh-TW": { translation: zhTW }, }, diff --git a/translations/ja.json b/translations/ja.json new file mode 100644 index 00000000..743f1e22 --- /dev/null +++ b/translations/ja.json @@ -0,0 +1,457 @@ +{ + "login": { + "username_required": "ユーザー名は必須です", + "error_title": "エラー", + "login_title": "ログイン", + "login_to_title": "ログイン先", + "username_placeholder": "ユーザー名", + "password_placeholder": "パスワード", + "login_button": "ログイン", + "quick_connect": "クイックコネクト", + "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", + "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", + "got_it": "了解", + "connection_failed": "接続に失敗しました", + "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", + "an_unexpected_error_occured": "予期しないエラーが発生しました", + "change_server": "サーバーの変更", + "invalid_username_or_password": "ユーザー名またはパスワードが無効です", + "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", + "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", + "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", + "there_is_a_server_error": "サーバーエラーが発生しました", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" + }, + "server": { + "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "接続", + "previous_servers": "前のサーバー", + "clear_button": "クリア", + "search_for_local_servers": "ローカルサーバーを検索", + "searching": "検索中...", + "servers": "サーバー" + }, + "home": { + "no_internet": "インターネット接続がありません", + "no_items": "アイテムはありません", + "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", + "go_to_downloads": "ダウンロードに移動", + "oops": "おっと!", + "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", + "continue_watching": "続きを見る", + "next_up": "次の動画", + "recently_added_in": "{{libraryName}}に最近追加された", + "suggested_movies": "おすすめ映画", + "suggested_episodes": "おすすめエピソード", + "intro": { + "welcome_to_streamyfin": "Streamyfinへようこそ", + "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", + "features_title": "特長", + "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", + "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", + "downloads_feature_title": "ダウンロード", + "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", + "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", + "centralised_settings_plugin_title": "集中設定プラグイン", + "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", + "done_button": "完了", + "go_to_settings_button": "設定に移動", + "read_more": "続きを読む" + }, + "settings": { + "settings_title": "設定", + "log_out_button": "ログアウト", + "user_info": { + "user_info_title": "ユーザー情報", + "user": "ユーザー", + "server": "サーバー", + "token": "トークン", + "app_version": "アプリバージョン" + }, + "quick_connect": { + "quick_connect_title": "クイックコネクト", + "authorize_button": "クイックコネクトを承認する", + "enter_the_quick_connect_code": "クイックコネクトコードを入力...", + "success": "成功しました", + "quick_connect_autorized": "クイックコネクトが承認されました", + "error": "エラー", + "invalid_code": "無効なコードです", + "authorize": "承認" + }, + "media_controls": { + "media_controls_title": "メディアコントロール", + "forward_skip_length": "スキップの長さ", + "rewind_length": "巻き戻しの長さ", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "オーディオ", + "set_audio_track": "前のアイテムからオーディオトラックを設定", + "audio_language": "オーディオ言語", + "audio_hint": "デフォルトのオーディオ言語を選択します。", + "none": "なし", + "language": "言語" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕の言語", + "subtitle_mode": "字幕モード", + "set_subtitle_track": "前のアイテムから字幕トラックを設定", + "subtitle_size": "字幕サイズ", + "subtitle_hint": "字幕設定を構成します。", + "none": "なし", + "language": "言語", + "loading": "ロード中", + "modes": { + "Default": "デフォルト", + "Smart": "スマート", + "Always": "常に", + "None": "なし", + "OnlyForced": "強制のみ" + } + }, + "other": { + "other_title": "その他", + "auto_rotate": "画面の自動回転", + "video_orientation": "動画の向き", + "orientation": "向き", + "orientations": { + "DEFAULT": "デフォルト", + "ALL": "すべて", + "PORTRAIT": "縦", + "PORTRAIT_UP": "縦向き(上)", + "PORTRAIT_DOWN": "縦方向", + "LANDSCAPE": "横方向", + "LANDSCAPE_LEFT": "横方向 左", + "LANDSCAPE_RIGHT": "横方向 右", + "OTHER": "その他", + "UNKNOWN": "不明" + }, + "safe_area_in_controls": "コントロールの安全エリア", + "show_custom_menu_links": "カスタムメニューのリンクを表示", + "hide_libraries": "ライブラリを非表示", + "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", + "disable_haptic_feedback": "触覚フィードバックを無効にする" + }, + "downloads": { + "downloads_title": "ダウンロード", + "download_method": "ダウンロード方法", + "remux_max_download": "Remux最大ダウンロード数", + "auto_download": "自動ダウンロード", + "optimized_versions_server": "Optimized versionsサーバー", + "save_button": "保存", + "optimized_server": "Optimizedサーバー", + "optimized": "最適化", + "default": "デフォルト", + "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート" + }, + "plugins": { + "plugins_title": "プラグイン", + "jellyseerr": { + "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", + "server_url": "サーバーURL", + "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "パスワード", + "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", + "save_button": "保存", + "clear_button": "クリア", + "login_button": "ログイン", + "total_media_requests": "メディアリクエストの合計", + "movie_quota_limit": "映画のクオータ制限", + "movie_quota_days": "映画のクオータ日数", + "tv_quota_limit": "テレビのクオータ制限", + "tv_quota_days": "テレビのクオータ日数", + "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", + "unlimited": "無制限" + }, + "marlin_search": { + "enable_marlin_search": "マーリン検索を有効にする ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート", + "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_marlin": "Marlinについて詳しく読む。", + "save_button": "保存", + "toasts": { + "saved": "保存しました" + } + } + }, + "storage": { + "storage_title": "ストレージ", + "app_usage": "アプリ {{usedSpace}}%", + "phone_usage": "電話 {{availableSpace}}%", + "size_used": "{{used}} / {{total}} 使用済み", + "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" + }, + "intro": { + "show_intro": "イントロを表示", + "reset_intro": "イントロをリセット" + }, + "logs": { + "logs_title": "ログ", + "no_logs_available": "ログがありません", + "delete_all_logs": "すべてのログを削除" + }, + "languages": { + "title": "言語", + "app_language": "アプリの言語", + "app_language_description": "アプリの言語を選択。", + "system": "システム" + }, + "toasts": { + "error_deleting_files": "ファイルの削除エラー", + "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", + "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", + "connected": "接続済み", + "could_not_connect": "接続できません", + "invalid_url": "無効なURL" + } + }, + "downloads": { + "downloads_title": "ダウンロード", + "tvseries": "TVシリーズ", + "movies": "映画", + "queue": "キュー", + "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", + "no_items_in_queue": "キューにアイテムがありません", + "no_downloaded_items": "ダウンロードしたアイテムはありません", + "delete_all_movies_button": "すべての映画を削除", + "delete_all_tvseries_button": "すべてのシリーズを削除", + "delete_all_button": "すべて削除", + "active_download": "アクティブなダウンロード", + "no_active_downloads": "アクティブなダウンロードはありません", + "active_downloads": "アクティブなダウンロード", + "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", + "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", + "back": "戻る", + "delete": "削除", + "something_went_wrong": "問題が発生しました", + "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", + "eta": "ETA {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", + "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", + "failed_to_delete_all_movies": "すべての映画を削除できませんでした", + "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", + "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", + "download_cancelled": "ダウンロードをキャンセルしました", + "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", + "download_completed": "ダウンロードが完了しました", + "download_started_for": "{{item}}のダウンロードが開始されました", + "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", + "download_stated_for_item": "{{item}}のダウンロードが開始されました", + "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", + "download_completed_for_item": "{{item}}のダウンロードが完了しました", + "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", + "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", + "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", + "no_response_received_from_server": "サーバーからの応答がありません", + "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", + "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", + "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", + "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", + "go_to_downloads": "ダウンロードに移動" + } + } + }, + "search": { + "search_here": "ここを検索...", + "search": "検索...", + "x_items": "{{count}}のアイテム", + "library": "ライブラリ", + "discover": "見つける", + "no_results": "結果はありません", + "no_results_found_for": "結果が見つかりませんでした:", + "movies": "映画", + "series": "シリーズ", + "episodes": "エピソード", + "collections": "コレクション", + "actors": "俳優", + "request_movies": "映画をリクエスト", + "request_series": "シリーズをリクエスト", + "recently_added": "最近の追加", + "recent_requests": "最近のリクエスト", + "plex_watchlist": "Plexウォッチリスト", + "trending": "トレンド", + "popular_movies": "人気の映画", + "movie_genres": "映画のジャンル", + "upcoming_movies": "今後リリースされる映画", + "studios": "制作会社", + "popular_tv": "人気のテレビ番組", + "tv_genres": "シリーズのジャンル", + "upcoming_tv": "今後リリースされるシリーズ", + "networks": "ネットワーク", + "tmdb_movie_keyword": "TMDB映画キーワード", + "tmdb_movie_genre": "TMDB映画ジャンル", + "tmdb_tv_keyword": "TMDBシリーズキーワード", + "tmdb_tv_genre": "TMDBシリーズジャンル", + "tmdb_search": "TMDB検索", + "tmdb_studio": "TMDB 制作会社", + "tmdb_network": "TMDB ネットワーク", + "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", + "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" + }, + "library": { + "no_items_found": "アイテムが見つかりません", + "no_results": "検索結果はありません", + "no_libraries_found": "ライブラリが見つかりません", + "item_types": { + "movies": "映画", + "series": "シリーズ", + "boxsets": "ボックスセット", + "items": "アイテム" + }, + "options": { + "display": "表示", + "row": "行", + "list": "リスト", + "image_style": "画像のスタイル", + "poster": "ポスター", + "cover": "カバー", + "show_titles": "タイトルの表示", + "show_stats": "統計を表示" + }, + "filters": { + "genres": "ジャンル", + "years": "年", + "sort_by": "ソート", + "sort_order": "ソート順", + "tags": "タグ" + } + }, + "favorites": { + "series": "シリーズ", + "movies": "映画", + "episodes": "エピソード", + "videos": "ビデオ", + "boxsets": "ボックスセット", + "playlists": "プレイリスト" + }, + "custom_links": { + "no_links": "リンクがありません" + }, + "player": { + "error": "エラー", + "failed_to_get_stream_url": "ストリームURLを取得できませんでした", + "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", + "client_error": "クライアントエラー", + "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", + "message_from_server": "サーバーからのメッセージ: {{message}}", + "video_has_finished_playing": "ビデオの再生が終了しました!", + "no_video_source": "動画ソースがありません...", + "next_episode": "次のエピソード", + "refresh_tracks": "トラックを更新", + "subtitle_tracks": "字幕トラック:", + "audio_tracks": "音声トラック:", + "playback_state": "再生状態:", + "no_data_available": "データなし", + "index": "インデックス:" + }, + "item_card": { + "next_up": "次", + "no_items_to_display": "表示するアイテムがありません", + "cast_and_crew": "キャスト&クルー", + "series": "シリーズ", + "seasons": "シーズン", + "season": "シーズン", + "no_episodes_for_this_season": "このシーズンのエピソードはありません", + "overview": "ストーリー", + "more_with": "{{name}}の詳細", + "similar_items": "類似アイテム", + "no_similar_items_found": "類似のアイテムは見つかりませんでした", + "video": "映像", + "more_details": "さらに詳細を表示", + "quality": "画質", + "audio": "音声", + "subtitles": "字幕", + "show_more": "もっと見る", + "show_less": "少なく表示", + "appeared_in": "出演作品", + "could_not_load_item": "アイテムを読み込めませんでした", + "none": "なし", + "download": { + "download_season": "シーズンをダウンロード", + "download_series": "シリーズをダウンロード", + "download_episode": "エピソードをダウンロード", + "download_movie": "映画をダウンロード", + "download_x_item": "{{item_count}}のアイテムをダウンロード", + "download_button": "ダウンロード", + "using_optimized_server": "Optimizeサーバーを使用する", + "using_default_method": "デフォルトの方法を使用" + } + }, + "live_tv": { + "next": "次", + "previous": "前", + "live_tv": "ライブTV", + "coming_soon": "近日公開", + "on_now": "現在", + "shows": "表示", + "movies": "映画", + "sports": "スポーツ", + "for_kids": "子供向け", + "news": "ニュース" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "キャンセル", + "yes": "はい", + "whats_wrong": "どうしましたか?", + "issue_type": "問題の種類", + "select_an_issue": "問題を選択", + "types": "種類", + "describe_the_issue": "(オプション) 問題を説明してください...", + "submit_button": "送信", + "report_issue_button": "チケットを報告", + "request_button": "リクエスト", + "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", + "failed_to_login": "ログインに失敗しました", + "cast": "出演者", + "details": "詳細", + "status": "状態", + "original_title": "原題", + "series_type": "シリーズタイプ", + "release_dates": "公開日", + "first_air_date": "初放送日", + "next_air_date": "次回放送日", + "revenue": "収益", + "budget": "予算", + "original_language": "オリジナルの言語", + "production_country": "制作国", + "studios": "制作会社", + "network": "ネットワーク", + "currently_streaming_on": "ストリーミング中", + "advanced": "詳細", + "request_as": "別ユーザーとしてリクエスト", + "tags": "タグ", + "quality_profile": "画質プロファイル", + "root_folder": "ルートフォルダ", + "season_x": "シーズン{{seasons}}", + "season_number": "シーズン{{season_number}}", + "number_episodes": "エピソード{{episode_number}}", + "born": "生まれ", + "appearances": "出演", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", + "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", + "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", + "issue_submitted": "チケットを送信しました!", + "requested_item": "{{item}}をリクエスト!", + "you_dont_have_permission_to_request": "リクエストする権限がありません!", + "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" + } + }, + "tabs": { + "home": "ホーム", + "search": "検索", + "library": "ライブラリ", + "custom_links": "カスタムリンク", + "favorites": "お気に入り" + } +} From 5b8418cd82ff0a1d270985b8661ae90205095e97 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 21 Feb 2025 13:14:57 +0100 Subject: [PATCH 02/93] feat: Sessions view (#537) --- .gitignore | 3 +- Makefile | 6 + app/(auth)/(tabs)/(home)/_layout.tsx | 63 +++- app/(auth)/(tabs)/(home)/sessions/index.tsx | 360 ++++++++++++++++++++ app/(auth)/(tabs)/(home)/settings.tsx | 7 + app/(auth)/(tabs)/_layout.tsx | 3 +- components/ItemTechnicalDetails.tsx | 22 +- components/list/ListGroup.tsx | 2 +- components/list/ListItem.tsx | 4 +- components/settings/Dashboard.tsx | 30 ++ hooks/useSessions.ts | 36 ++ login.yaml | 6 + translations/en.json | 14 +- utils/bitrate.ts | 8 + 14 files changed, 534 insertions(+), 30 deletions(-) create mode 100644 Makefile create mode 100644 app/(auth)/(tabs)/(home)/sessions/index.tsx create mode 100644 components/settings/Dashboard.tsx create mode 100644 hooks/useSessions.ts create mode 100644 login.yaml create mode 100644 utils/bitrate.ts diff --git a/.gitignore b/.gitignore index d7428e98..2a0ce8db 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.* *.orig.* web-build/ modules/vlc-player/android/build +bun.lockb # macOS .DS_Store @@ -42,4 +43,4 @@ credentials.json .vscode/ .idea/ .ruby-lsp -modules/hls-downloader/android/build \ No newline at end of file +modules/hls-downloader/android/build diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c2f6701a --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +e2e: + maestro start-device --platform android + maestro test login.yaml + +e2e-setup: +curl -fsSL "https://get.maestro.mobile.dev" | bash diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 0b9b9c11..7589d247 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -4,10 +4,15 @@ import { Stack, useRouter } from "expo-router"; import { Platform, TouchableOpacity, View } from "react-native"; import { useTranslation } from "react-i18next"; const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null; +import { useAtom } from "jotai"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { useSessions, useSessionsProps } from "@/hooks/useSessions"; export default function IndexLayout() { const router = useRouter(); + const [user] = useAtom(userAtom); const { t } = useTranslation(); + return ( - { - router.push("/(auth)/settings"); - }} - > - - + {user.Policy?.IsAdministrator && ( + + )} + )} @@ -52,6 +54,12 @@ export default function IndexLayout() { title: t("home.downloads.tvseries"), }} /> + + ); } + +const SettingsButton = () => { + const router = useRouter(); + + return ( + { + router.push("/(auth)/settings"); + }} + > + + + ); +}; + +const SessionsButton = () => { + const router = useRouter(); + const { sessions = [], _ } = useSessions({} as useSessionsProps); + + return ( + { + router.push("/(auth)/sessions"); + }} + > + + + + + ); +}; diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx new file mode 100644 index 00000000..097205da --- /dev/null +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -0,0 +1,360 @@ +import { Text } from "@/components/common/Text"; +import { useSessions, useSessionsProps } from "@/hooks/useSessions"; +import { FlashList } from "@shopify/flash-list"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { Loader } from "@/components/Loader"; +import { SessionInfoDto } from "@jellyfin/sdk/lib/generated-client"; +import { useAtomValue } from "jotai"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import Poster from "@/components/posters/Poster"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { useInterval } from "@/hooks/useInterval"; +import React, { useEffect, useMemo, useState } from "react"; +import { formatTimeString } from "@/utils/time"; +import { formatBitrate } from "@/utils/bitrate"; +import { + Ionicons, + Entypo, + AntDesign, + MaterialCommunityIcons, +} from "@expo/vector-icons"; +import { Badge } from "@/components/Badge"; + +export default function page() { + const { sessions, isLoading } = useSessions({} as useSessionsProps); + const { t } = useTranslation(); + + if (isLoading) + return ( + + + + ); + + if (!sessions || sessions.length == 0) + return ( + + + {t("home.sessions.no_active_sessions")} + + + ); + + return ( + } + keyExtractor={(item) => item.Id || ""} + estimatedItemSize={200} + /> + ); +} + +interface SessionCardProps { + session: SessionInfoDto; +} + +const SessionCard = ({ session }: SessionCardProps) => { + const api = useAtomValue(apiAtom); + const [remainingTicks, setRemainingTicks] = useState(0); + + const tick = () => { + if (session.PlayState?.IsPaused) return; + setRemainingTicks(remainingTicks - 10000000); + }; + + const getProgressPercentage = () => { + if (!session.NowPlayingItem || !session.NowPlayingItem.RunTimeTicks) { + return 0; + } + + return Math.round( + (100 / session.NowPlayingItem?.RunTimeTicks) * + (session.NowPlayingItem?.RunTimeTicks - remainingTicks) + ); + }; + + useEffect(() => { + const currentTime = session.PlayState?.PositionTicks; + const duration = session.NowPlayingItem?.RunTimeTicks; + if ( + duration !== null && + duration !== undefined && + currentTime !== null && + currentTime !== undefined + ) { + const remainingTimeTicks = duration - currentTime; + setRemainingTicks(remainingTimeTicks); + } + }, [session]); + + useInterval(tick, 1000); + + return ( + + + + + + + + + {session.NowPlayingItem?.Name} + {!session.NowPlayingItem?.SeriesName && ( + + {session.NowPlayingItem?.ProductionYear} + + )} + {session.NowPlayingItem?.SeriesName && ( + + {session.NowPlayingItem?.SeriesName} + + )} + + + {session.UserName} + {"\n"} + {session.Client} + {"\n"} + {session.DeviceName} + + + + + + + {!session.PlayState?.IsPaused ? ( + + ) : ( + + )} + + + {formatTimeString(remainingTicks, "tick")} left + + + + + + + + + + + ); +}; + +interface TranscodingBadgesProps { + properties: Array; +} + +const TranscodingBadges = ({ properties = [] }: TranscodingBadgesProps) => { + const icon = (val: string) => { + switch (val) { + case "bitrate": + return ; + break; + case "codec": + return ; + break; + case "videoRange": + return ( + + ); + break; + case "resolution": + return ; + break; + case "language": + return ; + break; + case "audioChannels": + return ; + break; + default: + return ; + } + }; + + const formatVal = (key: String, val: any) => { + switch (key) { + case "bitrate": + return formatBitrate(val); + break; + default: + return val; + } + }; + + return Object.keys(properties) + .filter( + (key) => !(properties[key] === undefined || properties[key] === null) + ) + .map((key) => ( + + )); +}; + +interface StreamProps { + resolution: String | null | undefined; + language: String | null | undefined; + codec: String | null | undefined; + bitrate: number | null | undefined; + videoRange: String | null | undefined; + audioChannels: String | null | undefined; +} + +interface TranscodingStreamViewProps { + title: String | undefined; + value: String; + isTranscoding: Boolean; + transcodeValue: String | undefined | null; + properties: Array; + transcodeProperties: Array; +} + +const TranscodingStreamView = ({ + title, + value, + isTranscoding, + transcodeValue, + properties = [], + transcodeProperties = [], +}: TranscodingStreamViewProps) => { + return ( + + + + {title} + + + + + + {isTranscoding && ( + <> + + + + + + + + + + )} + + ); +}; + +const TranscodingView = ({ session }: SessionCardProps) => { + const videoStream = useMemo(() => { + return session.NowPlayingItem?.MediaStreams?.filter( + (s) => s.Type == "Video" + )[0]; + }, [session]); + + const audioStream = useMemo(() => { + const index = session.PlayState?.AudioStreamIndex; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; + }, [session.PlayState?.AudioStreamIndex]); + + const subtitleStream = useMemo(() => { + const index = session.PlayState?.SubtitleStreamIndex; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; + }, [session.PlayState?.SubtitleStreamIndex]); + + const isTranscoding = useMemo(() => { + return session.PlayState?.PlayMethod == "Transcode"; + }, [session.PlayState?.PlayMethod]); + + const videoStreamTitle = () => { + return videoStream?.DisplayTitle?.split(" ")[0]; + }; + + return ( + + + + + + {subtitleStream && ( + <> + + + )} + + ); +}; diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index aba54ae1..49346baa 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -12,6 +12,7 @@ import { QuickConnect } from "@/components/settings/QuickConnect"; import { StorageSettings } from "@/components/settings/StorageSettings"; import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; +import { Dashboard } from "@/components/settings/Dashboard"; import { useHaptic } from "@/hooks/useHaptic"; import { useJellyfin } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; @@ -21,10 +22,13 @@ import { t } from "i18next"; import React, { useEffect } from "react"; import { ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useAtom } from "jotai"; +import { userAtom } from "@/providers/JellyfinProvider"; export default function settings() { const router = useRouter(); const insets = useSafeAreaInsets(); + const [user] = useAtom(userAtom); const { logout } = useJellyfin(); const successHapticFeedback = useHaptic("success"); @@ -59,6 +63,9 @@ export default function settings() { > + + {user && user.Policy?.IsAdministrator && } + diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 011ae3fa..1b789c62 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -10,7 +10,6 @@ import { } from "@bottom-tabs/react-navigation"; const { Navigator } = createNativeBottomTabNavigator(); - import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; import { Colors } from "@/constants/Colors"; @@ -138,4 +137,4 @@ export default function TabLayout() { ); -} +} \ No newline at end of file diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx index 6b5852a4..e2570dc8 100644 --- a/components/ItemTechnicalDetails.tsx +++ b/components/ItemTechnicalDetails.tsx @@ -16,6 +16,7 @@ import { } from "@gorhom/bottom-sheet"; import { Button } from "./Button"; import { useTranslation } from "react-i18next"; +import { formatBitrate } from "@/utils/bitrate"; interface Props { source?: MediaSourceInfo; @@ -54,14 +55,18 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { - {t("item_card.video")} + + {t("item_card.video")} + - {t("item_card.audio")} + + {t("item_card.audio")} + = ({ source, ...props }) => { - {t("item_card.subtitles")} + + {t("item_card.subtitles")} + { const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString()); return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i]; }; - -const formatBitrate = (bitrate?: number | null) => { - if (!bitrate) return "N/A"; - - const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; - if (bitrate === 0) return "0 bps"; - const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString()); - return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i]; -}; diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index c1d706a4..03f218d1 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -29,7 +29,7 @@ export const ListGroup: React.FC> = ({ {Children.map(childrenArray, (child, index) => { if (isValidElement<{ style?: ViewStyle }>(child)) { diff --git a/components/list/ListItem.tsx b/components/list/ListItem.tsx index 403b33dc..ea7774a4 100644 --- a/components/list/ListItem.tsx +++ b/components/list/ListItem.tsx @@ -36,7 +36,7 @@ export const ListItem: React.FC> = ({ > = ({ ); return ( { + const [settings, updateSettings] = useSettings(); + const { sessions = [], isLoading } = useSessions({} as useSessionsProps); + const router = useRouter(); + + const { t } = useTranslation(); + + if (!settings) return null; + return ( + + + router.push("/settings/dashboard/sessions")} + title={t("home.settings.dashboard.sessions_title")} + showArrow + /> + + + ); +}; diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts new file mode 100644 index 00000000..ec8d3189 --- /dev/null +++ b/hooks/useSessions.ts @@ -0,0 +1,36 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { useAtom } from "jotai"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; +import { userAtom } from "@/providers/JellyfinProvider"; + +export interface useSessionsProps { + refetchInterval: number; + activeWithinSeconds: number; +} + +export const useSessions = ({ + refetchInterval = 5 * 1000, + activeWithinSeconds = 360, +}: useSessionsProps) => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const { data, isLoading, error } = useQuery({ + queryKey: ["sessions"], + queryFn: async () => { + if (!api || !user || !user.Policy?.IsAdministrator) { + return []; + } + const response = await getSessionApi(api).getSessions({ + activeWithinSeconds: activeWithinSeconds, + }); + return response.data.filter((s) => s.NowPlayingItem); + }, + refetchInterval: refetchInterval, + //enabled: !!user || !!user.Policy?.IsAdministrator, + //cacheTime: 0 + }); + + return { sessions: data, isLoading }; +}; diff --git a/login.yaml b/login.yaml new file mode 100644 index 00000000..54418a9b --- /dev/null +++ b/login.yaml @@ -0,0 +1,6 @@ +# login.yaml + +appId: your.app.id +--- +- launchApp +- tapOn: "Text on the screen" diff --git a/translations/en.json b/translations/en.json index d2f54e99..615c3e40 100644 --- a/translations/en.json +++ b/translations/en.json @@ -147,7 +147,7 @@ "default": "Default", "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", "read_more_about_optimized_server": "Read more about the optimize server.", - "url":"URL", + "url": "URL", "server_url_placeholder": "http(s)://domain.org:port" }, "plugins": { @@ -204,14 +204,18 @@ "app_language_description": "Select the language for the app.", "system": "System" }, - "toasts":{ + "toasts": { "error_deleting_files": "Error deleting files", "background_downloads_enabled": "Background downloads enabled", "background_downloads_disabled": "Background downloads disabled", "connected": "Connected", "could_not_connect": "Could not connect", "invalid_url": "Invalid URL" - } + }, + }, + "sessions": { + "title": "Sessions", + "no_active_sessions": "No active sessions" }, "downloads": { "downloads_title": "Downloads", @@ -399,7 +403,7 @@ "for_kids": "For Kids", "news": "News" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirm", "cancel": "Cancel", "yes": "Yes", @@ -455,4 +459,4 @@ "custom_links": "Custom Links", "favorites": "Favorites" } -} +} \ No newline at end of file diff --git a/utils/bitrate.ts b/utils/bitrate.ts new file mode 100644 index 00000000..7f1d0f47 --- /dev/null +++ b/utils/bitrate.ts @@ -0,0 +1,8 @@ +export const formatBitrate = (bitrate?: number | null) => { + if (!bitrate) return "N/A"; + + const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; + if (bitrate === 0) return "0 bps"; + const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString()); + return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i]; +}; From 04dce9265b0548d755feac81c26dccd82fadc94c Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 21 Feb 2025 14:56:59 +0100 Subject: [PATCH 03/93] Update _layout.tsx --- app/(auth)/(tabs)/(home)/_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 7589d247..2eefecce 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -32,7 +32,7 @@ export default function IndexLayout() { {!Platform.isTV && ( <> - {user.Policy?.IsAdministrator && ( + {user && user.Policy?.IsAdministrator && ( )} From ce38024a3fe0959e7e3c4c15066a26845a516f3d Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 21 Feb 2025 14:57:53 +0100 Subject: [PATCH 04/93] Update settings.tsx --- app/(auth)/(tabs)/(home)/settings.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 49346baa..1818260f 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -12,7 +12,6 @@ import { QuickConnect } from "@/components/settings/QuickConnect"; import { StorageSettings } from "@/components/settings/StorageSettings"; import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; -import { Dashboard } from "@/components/settings/Dashboard"; import { useHaptic } from "@/hooks/useHaptic"; import { useJellyfin } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; @@ -64,8 +63,6 @@ export default function settings() { - {user && user.Policy?.IsAdministrator && } - From b98a7b0634c7ad6e2f23362a268c038574e601ed Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 21 Feb 2025 18:22:05 +0100 Subject: [PATCH 05/93] Update _layout.tsx --- app/(auth)/(tabs)/(home)/_layout.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 2eefecce..525e1f22 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,5 +1,5 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; -import { Feather } from "@expo/vector-icons"; +import { Ionicons, Feather } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; import { Platform, TouchableOpacity, View } from "react-native"; import { useTranslation } from "react-i18next"; @@ -152,9 +152,9 @@ const SessionsButton = () => { }} > - From b478fbb6bfeeeafcf31fd2f9774975a5f800683c Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 21 Feb 2025 20:38:31 +0100 Subject: [PATCH 06/93] fix: tvos fixes --- .../series/[id].tsx | 34 +- app/(auth)/player/_layout.tsx | 5 + app/_layout.tsx | 1 + bun.lock | 4 +- components/DownloadItem.tsx | 33 +- components/ItemContent.tsx | 13 +- components/PlayButton.tsx | 310 +++++++++--------- components/PlayButton.tv.tsx | 117 +++---- components/common/Text.tsx | 28 +- components/downloads/ActiveDownloads.tsx | 41 ++- components/video-player/controls/Controls.tsx | 24 +- hooks/useRemuxHlsToMp4.ts | 41 ++- modules/vlc-player/ios/VlcPlayer.podspec | 2 +- package.json | 2 +- 14 files changed, 337 insertions(+), 318 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx index a62405e1..9a284d33 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -15,7 +15,7 @@ import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; import React, { useEffect, useMemo } from "react"; -import { View } from "react-native"; +import { Platform, View } from "react-native"; import { useTranslation } from "react-i18next"; const page: React.FC = () => { @@ -85,21 +85,25 @@ const page: React.FC = () => { allEpisodes.length > 0 && ( - ( - - )} - DownloadedIconComponent={() => ( - + ( + + )} + DownloadedIconComponent={() => ( + + )} /> - )} - /> + + )} ), }); diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index bee527e5..0eefb300 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -3,16 +3,21 @@ import React, { useEffect } from "react"; import { SystemBars } from "react-native-edge-to-edge"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useSettings } from "@/utils/atoms/settings"; +import { Platform } from "react-native"; export default function Layout() { const [settings] = useSettings(); useEffect(() => { + if (Platform.isTV) return; + if (settings.defaultVideoOrientation) { ScreenOrientation.lockAsync(settings.defaultVideoOrientation); } return () => { + if (Platform.isTV) return; + if (settings.autoRotate === true) { ScreenOrientation.unlockAsync(); } else { diff --git a/app/_layout.tsx b/app/_layout.tsx index 0d061be5..ad84ba0c 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -270,6 +270,7 @@ function Layout() { useEffect(() => { // If the user has auto rotate enabled, unlock the orientation + if (Platform.isTV) return; if (settings.autoRotate === true) { ScreenOrientation.unlockAsync(); } else { diff --git a/bun.lock b/bun.lock index 33b28594..74136017 100644 --- a/bun.lock +++ b/bun.lock @@ -60,7 +60,7 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.0-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.7", + "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", @@ -1826,7 +1826,7 @@ "react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="], - "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.7", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cVQYs4r8Hb9V9oOO/SqsmBaZ7IzE/3Tpvz4mmRjNXKi1cBWC+ZpKTuqRx6EPjBCYTVK+vbAfoTM6IHS+6NVg4w=="], + "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="], "react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="], diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index befc34ca..e7286023 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { queueActions, queueAtom } from "@/utils/atoms/queue"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; @@ -21,7 +21,7 @@ import { import { Href, router, useFocusEffect } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo, useRef, useState } from "react"; -import { Alert, View, ViewProps } from "react-native"; +import { Alert, Platform, View, ViewProps } from "react-native"; import { toast } from "sonner-native"; import { AudioTrackSelector } from "./AudioTrackSelector"; import { Bitrate, BitrateSelector } from "./BitrateSelector"; @@ -66,10 +66,12 @@ export const DownloadItems: React.FC = ({ const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(0); - const [maxBitrate, setMaxBitrate] = useState(settings?.defaultBitrate ?? { - key: "Max", - value: undefined, - }); + const [maxBitrate, setMaxBitrate] = useState( + settings?.defaultBitrate ?? { + key: "Max", + value: undefined, + } + ); const userCanDownload = useMemo( () => user?.Policy?.EnableContentDownloading, @@ -162,7 +164,9 @@ export const DownloadItems: React.FC = ({ ); } } else { - toast.error(t("home.downloads.toasts.you_are_not_allowed_to_download_files")); + toast.error( + t("home.downloads.toasts.you_are_not_allowed_to_download_files") + ); } }, [ queue, @@ -333,7 +337,10 @@ export const DownloadItems: React.FC = ({ {title} - {subtitle || t("item_card.download.download_x_item", {item_count: itemsNotDownloaded.length})} + {subtitle || + t("item_card.download.download_x_item", { + item_count: itemsNotDownloaded.length, + })} @@ -391,12 +398,16 @@ export const DownloadSingleItem: React.FC<{ size?: "default" | "large"; item: BaseItemDto; }> = ({ item, size = "default" }) => { + if (Platform.isTV) return; + return ( ( diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 39aa1660..8fb538dd 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -15,6 +15,7 @@ import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarous import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import { useImageColors } from "@/hooks/useImageColors"; import { useOrientation } from "@/hooks/useOrientation"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; @@ -24,17 +25,16 @@ import { } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useEffect, useMemo, useState } from "react"; import { Platform, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -const Chromecast = !Platform.isTV ? require("./Chromecast") : null; +import { AddToFavorites } from "./AddToFavorites"; import { ItemHeader } from "./ItemHeader"; import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; -import { AddToFavorites } from "./AddToFavorites"; +const Chromecast = !Platform.isTV ? require("./Chromecast") : null; export type SelectedOptions = { bitrate: Bitrate; @@ -94,7 +94,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( /> {item.Type !== "Program" && ( - + {!Platform.isTV && ( + + )} @@ -164,7 +166,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( } > - {/* {!Platform.isTV && ( */} {item.Type !== "Program" && !Platform.isTV && ( @@ -222,13 +223,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - {/* {!Platform.isTV && ( */} - {/* )} */} {item.Type === "Episode" && ( diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index 66d1c434..ff8ecab4 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -1,4 +1,4 @@ -import { Platform } from "react-native"; +import { Platform, Pressable } from "react-native"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { useSettings } from "@/utils/atoms/settings"; @@ -79,6 +79,7 @@ export const PlayButton: React.FC = ({ ); const onPress = useCallback(async () => { + console.log("onPress"); if (!item) return; lightHapticFeedback(); @@ -113,105 +114,103 @@ export const PlayButton: React.FC = ({ switch (selectedIndex) { case 0: - if (!Platform.isTV) { - await CastContext.getPlayServicesState().then(async (state) => { - if (state && state !== PlayServicesState.SUCCESS) { - CastContext.showPlayServicesErrorDialog(state); - } else { - // Get a new URL with the Chromecast device profile: - try { - const data = await getStreamUrl({ - api, - item, - deviceProfile: chromecastProfile, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, - audioStreamIndex: selectedOptions.audioIndex, - maxStreamingBitrate: selectedOptions.bitrate?.value, - mediaSourceId: selectedOptions.mediaSource?.Id, - subtitleStreamIndex: selectedOptions.subtitleIndex, - }); + await CastContext.getPlayServicesState().then(async (state) => { + if (state && state !== PlayServicesState.SUCCESS) { + CastContext.showPlayServicesErrorDialog(state); + } else { + // Get a new URL with the Chromecast device profile: + try { + const data = await getStreamUrl({ + api, + item, + deviceProfile: chromecastProfile, + startTimeTicks: item?.UserData?.PlaybackPositionTicks!, + userId: user?.Id, + audioStreamIndex: selectedOptions.audioIndex, + maxStreamingBitrate: selectedOptions.bitrate?.value, + mediaSourceId: selectedOptions.mediaSource?.Id, + subtitleStreamIndex: selectedOptions.subtitleIndex, + }); - if (!data?.url) { - console.warn("No URL returned from getStreamUrl", data); - Alert.alert( - t("player.client_error"), - t("player.could_not_create_stream_for_chromecast") - ); - return; - } - - client - .loadMedia({ - mediaInfo: { - contentUrl: data?.url, - contentType: "video/mp4", - metadata: - item.Type === "Episode" - ? { - type: "tvShow", - title: item.Name || "", - episodeNumber: item.IndexNumber || 0, - seasonNumber: item.ParentIndexNumber || 0, - seriesTitle: item.SeriesName || "", - images: [ - { - url: getParentBackdropImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : item.Type === "Movie" - ? { - type: "movie", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : { - type: "generic", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - }, - }, - startTime: 0, - }) - .then(() => { - // state is already set when reopening current media, so skip it here. - if (isOpeningCurrentlyPlayingMedia) { - return; - } - CastContext.showExpandedControls(); - }); - } catch (e) { - console.log(e); + if (!data?.url) { + console.warn("No URL returned from getStreamUrl", data); + Alert.alert( + t("player.client_error"), + t("player.could_not_create_stream_for_chromecast") + ); + return; } + + client + .loadMedia({ + mediaInfo: { + contentUrl: data?.url, + contentType: "video/mp4", + metadata: + item.Type === "Episode" + ? { + type: "tvShow", + title: item.Name || "", + episodeNumber: item.IndexNumber || 0, + seasonNumber: item.ParentIndexNumber || 0, + seriesTitle: item.SeriesName || "", + images: [ + { + url: getParentBackdropImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + } + : item.Type === "Movie" + ? { + type: "movie", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + } + : { + type: "generic", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + }, + }, + startTime: 0, + }) + .then(() => { + // state is already set when reopening current media, so skip it here. + if (isOpeningCurrentlyPlayingMedia) { + return; + } + CastContext.showExpandedControls(); + }); + } catch (e) { + console.log(e); } - }); - } + } + }); break; case 1: goToPlayer(queryString, selectedOptions.bitrate?.value); @@ -323,75 +322,62 @@ export const PlayButton: React.FC = ({ */ return ( - - - - - - + + - - - - {runtimeTicksToMinutes(item?.RunTimeTicks)} - + + + + + + + {runtimeTicksToMinutes(item?.RunTimeTicks)} + + + + + {client && ( - + + - {client && ( - - - - - )} - {!client && settings?.openInVLC && ( - - - - )} - + )} + {!client && settings?.openInVLC && ( + + + + )} - - {/* - - - {directStream ? "Direct stream" : "Transcoded stream"} - - */} - + + ); }; diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 128c2184..1fb0563c 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -63,7 +63,8 @@ export const PlayButton: React.FC = ({ [router] ); - const onPress = useCallback(async () => { + const onPress = () => { + console.log("onpress"); if (!item) return; lightHapticFeedback(); @@ -79,15 +80,7 @@ export const PlayButton: React.FC = ({ const queryString = queryParams.toString(); goToPlayer(queryString, selectedOptions.bitrate?.value); return; - }, [ - item, - settings, - api, - user, - router, - showActionSheetWithOptions, - selectedOptions, - ]); + }; const derivedTargetWidth = useDerivedValue(() => { if (!item || !item.RunTimeTicks) return 0; @@ -179,69 +172,55 @@ export const PlayButton: React.FC = ({ */ return ( - - - - - - + + - - - - {runtimeTicksToMinutes(item?.RunTimeTicks)} - + + + + + + + {runtimeTicksToMinutes(item?.RunTimeTicks)} + + + + + {settings?.openInVLC && ( - + - {settings?.openInVLC && ( - - - - )} - + )} - - {/* - - - {directStream ? "Direct stream" : "Transcoded stream"} - - */} - + + ); }; diff --git a/components/common/Text.tsx b/components/common/Text.tsx index ef7a6491..624b9da6 100644 --- a/components/common/Text.tsx +++ b/components/common/Text.tsx @@ -1,19 +1,27 @@ import React from "react"; -import { TextProps } from "react-native"; +import { Platform, TextProps } from "react-native"; import { UITextView } from "react-native-uitextview"; - +import { Text as RNText } from "react-native"; export function Text( props: TextProps & { uiTextView?: boolean; } ) { const { style, ...otherProps } = props; - - return ( - - ); + if (Platform.isTV) + return ( + + ); + else + return ( + + ); } diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 7b6316f8..773efab4 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,16 +1,15 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; +import { storage } from "@/utils/mmkv"; import { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; -const BackGroundDownloader = !Platform.isTV - ? require("@kesha-antonov/react-native-background-downloader") - : null; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Image } from "expo-image"; import { useRouter } from "expo-router"; -const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; -import { useAtom } from "jotai"; +import { t } from "i18next"; +import { useMemo } from "react"; import { ActivityIndicator, Platform, @@ -21,10 +20,12 @@ import { } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; -import { Image } from "expo-image"; -import { useMemo } from "react"; -import { storage } from "@/utils/mmkv"; -import { t } from "i18next"; +const BackGroundDownloader = !Platform.isTV + ? require("@kesha-antonov/react-native-background-downloader") + : null; +const FFmpegKitProvider = !Platform.isTV + ? require("ffmpeg-kit-react-native") + : null; interface Props extends ViewProps {} @@ -33,14 +34,20 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { if (processes?.length === 0) return ( - {t("home.downloads.active_download")} - {t("home.downloads.no_active_downloads")} + + {t("home.downloads.active_download")} + + + {t("home.downloads.no_active_downloads")} + ); return ( - {t("home.downloads.active_downloads")} + + {t("home.downloads.active_downloads")} + {processes?.map((p: JobStatus) => ( @@ -81,7 +88,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } } else { FFmpegKitProvider.FFmpegKit.cancel(Number(id)); - setProcesses((prev: any[]) => prev.filter((p: { id: string; }) => p.id !== id)); + setProcesses((prev: any[]) => + prev.filter((p: { id: string }) => p.id !== id) + ); } }, onSuccess: () => { @@ -156,7 +165,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { {process.speed?.toFixed(2)}x )} {eta(process) && ( - {t("home.downloads.eta", {eta: eta(process)})} + + {t("home.downloads.eta", { eta: eta(process) })} + )} diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 3232965d..17bd2028 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -540,17 +540,19 @@ export const Controls: React.FC = ({ pointerEvents={showControls ? "auto" : "none"} className={`flex flex-row w-full pt-2`} > - - - - - + {!Platform.isTV && ( + + + + + + )} {!Platform.isTV && ( diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index 925d9778..22ea02ce 100644 --- a/hooks/useRemuxHlsToMp4.ts +++ b/hooks/useRemuxHlsToMp4.ts @@ -11,7 +11,9 @@ import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; // import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native"; -const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; +const FFMPEGKitReactNative = !Platform.isTV + ? require("ffmpeg-kit-react-native") + : null; import { useAtomValue } from "jotai"; import { useCallback } from "react"; import { toast } from "sonner-native"; @@ -24,8 +26,10 @@ import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession; -type Statistics = typeof FFMPEGKitReactNative.Statistics -const FFmpegKit = FFMPEGKitReactNative.FFmpegKit; +type Statistics = typeof FFMPEGKitReactNative.Statistics; +const FFmpegKit = Platform.isTV + ? null + : (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit); const createFFmpegCommand = (url: string, output: string) => [ "-y", // overwrite output files without asking "-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options @@ -101,7 +105,10 @@ export const useRemuxHlsToMp4 = () => { } setProcesses((prev: any[]) => { - return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id); + return prev.filter( + (process: { itemId: string | undefined }) => + process.itemId !== item.Id + ); }); } catch (e) { console.error(e); @@ -126,7 +133,7 @@ export const useRemuxHlsToMp4 = () => { if (!item.Id) throw new Error("Item is undefined"); setProcesses((prev: any[]) => { - return prev.map((process: { itemId: string | undefined; }) => { + return prev.map((process: { itemId: string | undefined }) => { if (process.itemId === item.Id) { return { ...process, @@ -161,15 +168,18 @@ export const useRemuxHlsToMp4 = () => { // First lets save any important assets we want to present to the user offline await onSaveAssets(api, item); - toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), { - action: { - label: "Go to download", - onClick: () => { - router.push("/downloads"); - toast.dismiss(); + toast.success( + t("home.downloads.toasts.download_started_for", { item: item.Name }), + { + action: { + label: "Go to download", + onClick: () => { + router.push("/downloads"); + toast.dismiss(); + }, }, - }, - }); + } + ); try { const job: JobStatus = { @@ -201,7 +211,10 @@ export const useRemuxHlsToMp4 = () => { Error: ${error.message}, Stack: ${error.stack}` ); setProcesses((prev: any[]) => { - return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id); + return prev.filter( + (process: { itemId: string | undefined }) => + process.itemId !== item.Id + ); }); throw error; // Re-throw the error to propagate it to the caller } diff --git a/modules/vlc-player/ios/VlcPlayer.podspec b/modules/vlc-player/ios/VlcPlayer.podspec index 97f58881..25f2d73e 100644 --- a/modules/vlc-player/ios/VlcPlayer.podspec +++ b/modules/vlc-player/ios/VlcPlayer.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| s.description = 'A sample project description' s.author = '' s.homepage = 'https://docs.expo.dev/modules/' - s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.platforms = { :ios => '13.4', :tvos => '16' } s.source = { git: '' } s.static_framework = true diff --git a/package.json b/package.json index a04b6b4b..074e60bd 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.0-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.7", + "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", From cca0bbf42cbb92d55553697dd4cf734ed13791a0 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Sat, 22 Feb 2025 10:58:28 +0100 Subject: [PATCH 07/93] bigger play button --- app/(auth)/(tabs)/(home)/_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 525e1f22..ddcd6f7b 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -155,7 +155,7 @@ const SessionsButton = () => { From bd2aeb2234629506efdd428d5b1a127fe42a2bca Mon Sep 17 00:00:00 2001 From: vuhe Date: Sat, 22 Feb 2025 18:06:10 +0800 Subject: [PATCH 08/93] feat: Add Chinese (Simplified) Translation (#556) --- i18n.ts | 3 + translations/zh-CN.json | 457 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 460 insertions(+) create mode 100644 translations/zh-CN.json diff --git a/i18n.ts b/i18n.ts index 99ba26a7..973b1268 100644 --- a/i18n.ts +++ b/i18n.ts @@ -9,6 +9,7 @@ import it from "./translations/it.json"; import ja from "./translations/ja.json"; import nl from "./translations/nl.json"; import sv from "./translations/sv.json"; +import zhCN from './translations/zh-CN.json'; import zhTW from './translations/zh-TW.json'; import { getLocales } from "expo-localization"; @@ -21,6 +22,7 @@ export const APP_LANGUAGES = [ { label: "日本語", value: "ja" }, { label: "Nederlands", value: "nl" }, { label: "Svenska", value: "sv" }, + { label: "简体中文", value: "zh-CN" }, { label: "繁體中文", value: "zh-TW" }, ]; @@ -35,6 +37,7 @@ i18n.use(initReactI18next).init({ ja: { translation: ja }, nl: { translation: nl }, sv: { translation: sv }, + "zh-CN": { translation: zhCN }, "zh-TW": { translation: zhTW }, }, diff --git a/translations/zh-CN.json b/translations/zh-CN.json new file mode 100644 index 00000000..4e04ad6f --- /dev/null +++ b/translations/zh-CN.json @@ -0,0 +1,457 @@ +{ + "login": { + "username_required": "需要用户名", + "error_title": "错误", + "login_title": "登录", + "login_to_title": "登录至", + "username_placeholder": "用户名", + "password_placeholder": "密码", + "login_button": "登录", + "quick_connect": "快速连接", + "enter_code_to_login": "输入代码 {{code}} 以登录", + "failed_to_initiate_quick_connect": "无法启动快速连接", + "got_it": "了解", + "connection_failed": "连接失败", + "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", + "an_unexpected_error_occured": "发生意外错误", + "change_server": "更改服务器", + "invalid_username_or_password": "无效的用户名或密码", + "user_does_not_have_permission_to_log_in": "用户没有登录权限", + "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", + "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", + "there_is_a_server_error": "服务器出错", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "连接", + "previous_servers": "上一个服务器", + "clear_button": "清除", + "search_for_local_servers": "搜索本地服务器", + "searching": "搜索中...", + "servers": "服务器" + }, + "home": { + "no_internet": "无网络", + "no_items": "无项目", + "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", + "go_to_downloads": "前往下载", + "oops": "哎呀!", + "error_message": "出错了。\n请注销重新登录。", + "continue_watching": "继续观看", + "next_up": "下一个", + "recently_added_in": "最近添加于 {{libraryName}}", + "suggested_movies": "推荐电影", + "suggested_episodes": "推荐剧集", + "intro": { + "welcome_to_streamyfin": "欢迎来到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", + "features_title": "功能", + "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", + "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", + "downloads_feature_title": "下载", + "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", + "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", + "centralised_settings_plugin_title": "统一设置插件", + "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", + "done_button": "完成", + "go_to_settings_button": "前往设置", + "read_more": "了解更多" + }, + "settings": { + "settings_title": "设置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用户信息", + "user": "用户", + "server": "服务器", + "token": "密钥", + "app_version": "应用版本" + }, + "quick_connect": { + "quick_connect_title": "快速连接", + "authorize_button": "授权快速连接", + "enter_the_quick_connect_code": "输入快速连接代码...", + "success": "成功", + "quick_connect_autorized": "快速连接已授权", + "error": "错误", + "invalid_code": "无效代码", + "authorize": "授权" + }, + "media_controls": { + "media_controls_title": "媒体控制", + "forward_skip_length": "快进时长", + "rewind_length": "快退时长", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音频", + "set_audio_track": "从上一个项目设置音轨", + "audio_language": "音频语言", + "audio_hint": "选择默认音频语言。", + "none": "无", + "language": "语言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕语言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "从上一个项目设置字幕", + "subtitle_size": "字幕大小", + "subtitle_hint": "设置字幕偏好。", + "none": "无", + "language": "语言", + "loading": "加载中", + "modes": { + "Default": "默认", + "Smart": "智能", + "Always": "总是", + "None": "无", + "OnlyForced": "仅强制字幕" + } + }, + "other": { + "other_title": "其他", + "auto_rotate": "自动旋转", + "video_orientation": "视频方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默认", + "ALL": "全部", + "PORTRAIT": "纵向", + "PORTRAIT_UP": "纵向向上", + "PORTRAIT_DOWN": "纵向向下", + "LANDSCAPE": "横向", + "LANDSCAPE_LEFT": "横向左", + "LANDSCAPE_RIGHT": "横向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全区域", + "show_custom_menu_links": "显示自定义菜单链接", + "hide_libraries": "隐藏媒体库", + "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", + "disable_haptic_feedback": "禁用触觉反馈" + }, + "downloads": { + "downloads_title": "下载", + "download_method": "下载方法", + "remux_max_download": "Remux 最大下载", + "auto_download": "自动下载", + "optimized_versions_server": "Optimized Version 服务器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已优化", + "default": "默认", + "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", + "server_url": "服务器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密码", + "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登录", + "total_media_requests": "总媒体请求", + "movie_quota_limit": "电影配额限制", + "movie_quota_days": "电影配额天数", + "tv_quota_limit": "剧集配额限制", + "tv_quota_days": "剧集配额天数", + "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", + "unlimited": "无限制" + }, + "marlin_search": { + "enable_marlin_search": "启用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_marlin": "查看更多关于 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存储", + "app_usage": "应用 {{usedSpace}}%", + "device_usage": "设备 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "删除所有已下载文件" + }, + "intro": { + "show_intro": "显示介绍", + "reset_intro": "重置介绍" + }, + "logs": { + "logs_title": "日志", + "no_logs_available": "无可用日志", + "delete_all_logs": "删除所有日志" + }, + "languages": { + "title": "语言", + "app_language": "应用语言", + "app_language_description": "选择应用的语言。", + "system": "系统" + }, + "toasts": { + "error_deleting_files": "删除文件时出错", + "background_downloads_enabled": "后台下载已启用", + "background_downloads_disabled": "后台下载已禁用", + "connected": "已连接", + "could_not_connect": "无法连接", + "invalid_url": "无效 URL" + } + }, + "downloads": { + "downloads_title": "下载", + "tvseries": "剧集", + "movies": "电影", + "queue": "队列", + "queue_hint": "应用重启后队列和下载将会丢失", + "no_items_in_queue": "队列中无项目", + "no_downloaded_items": "无已下载项目", + "delete_all_movies_button": "删除所有电影", + "delete_all_tvseries_button": "删除所有剧集", + "delete_all_button": "删除全部", + "active_download": "活跃下载", + "no_active_downloads": "无活跃下载", + "active_downloads": "活跃下载", + "new_app_version_requires_re_download": "更新版本需要重新下载", + "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", + "back": "返回", + "delete": "删除", + "something_went_wrong": "出现问题", + "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", + "eta": "预计完成时间 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您无权下载文件。", + "deleted_all_movies_successfully": "成功删除所有电影!", + "failed_to_delete_all_movies": "删除所有电影失败", + "deleted_all_tvseries_successfully": "成功删除所有剧集!", + "failed_to_delete_all_tvseries": "删除所有剧集失败", + "download_cancelled": "下载已取消", + "could_not_cancel_download": "无法取消下载", + "download_completed": "下载完成", + "download_started_for": "开始下载 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", + "download_stated_for_item": "开始下载 {{item}}", + "download_failed_for_item": "下载失败 {{item}} - {{error}}", + "download_completed_for_item": "下载完成 {{item}}", + "queued_item_for_optimization": "已将 {{item}} 队列进行优化", + "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", + "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", + "no_response_received_from_server": "未收到服务器响应", + "error_setting_up_the_request": "设置请求时出错", + "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", + "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", + "go_to_downloads": "前往下载" + } + } + }, + "search": { + "search_here": "在此搜索...", + "search": "搜索...", + "x_items": "{{count}} 项目", + "library": "媒体库", + "discover": "发现", + "no_results": "没有结果", + "no_results_found_for": "未找到结果", + "movies": "电影", + "series": "剧集", + "episodes": "单集", + "collections": "收藏", + "actors": "演员", + "request_movies": "请求电影", + "request_series": "请求系列", + "recently_added": "最近添加", + "recent_requests": "最近请求", + "plex_watchlist": "Plex 观影清单", + "trending": "趋势", + "popular_movies": "热门电影", + "movie_genres": "电影类型", + "upcoming_movies": "即将上映的电影", + "studios": "工作室", + "popular_tv": "热门电影", + "tv_genres": "剧集类型", + "upcoming_tv": "即将上映的剧集", + "networks": "网络", + "tmdb_movie_keyword": "TMDB 电影关键词", + "tmdb_movie_genre": "TMDB 电影类型", + "tmdb_tv_keyword": "TMDB 剧集关键词", + "tmdb_tv_genre": "TMDB 剧集类型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 网络", + "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", + "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" + }, + "library": { + "no_items_found": "未找到项目", + "no_results": "没有结果", + "no_libraries_found": "未找到媒体库", + "item_types": { + "movies": "电影", + "series": "剧集", + "boxsets": "套装", + "items": "项" + }, + "options": { + "display": "显示", + "row": "行", + "list": "列表", + "image_style": "图片样式", + "poster": "海报", + "cover": "封面", + "show_titles": "显示标题", + "show_stats": "显示统计" + }, + "filters": { + "genres": "类型", + "years": "年份", + "sort_by": "排序依据", + "sort_order": "排序顺序", + "tags": "标签" + } + }, + "favorites": { + "series": "剧集", + "movies": "电影", + "episodes": "单集", + "videos": "视频", + "boxsets": "套装", + "playlists": "播放列表" + }, + "custom_links": { + "no_links": "无链接" + }, + "player": { + "error": "错误", + "failed_to_get_stream_url": "无法获取流 URL", + "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", + "client_error": "客户端错误", + "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", + "message_from_server": "来自服务器的消息:{{message}}", + "video_has_finished_playing": "视频播放完成!", + "no_video_source": "无视频来源...", + "next_episode": "下一集", + "refresh_tracks": "刷新轨道", + "subtitle_tracks": "字幕轨道:", + "audio_tracks": "音频轨道:", + "playback_state": "播放状态:", + "no_data_available": "无可用数据", + "index": "索引:" + }, + "item_card": { + "next_up": "下一个", + "no_items_to_display": "无项目显示", + "cast_and_crew": "演员和工作人员", + "series": "剧集", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季无剧集", + "overview": "概览", + "more_with": "更多 {{name}} 的作品", + "similar_items": "类似项目", + "no_similar_items_found": "未找到类似项目", + "video": "视频", + "more_details": "更多详情", + "quality": "质量", + "audio": "音频", + "subtitles": "字幕", + "show_more": "显示更多", + "show_less": "显示更少", + "appeared_in": "出现于", + "could_not_load_item": "无法加载项目", + "none": "无", + "download": { + "download_season": "下载季", + "download_series": "下载剧集", + "download_episode": "下载单集", + "download_movie": "下载电影", + "download_x_item": "下载 {{item_count}} 项目", + "download_button": "下载", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默认方法" + } + }, + "live_tv": { + "next": "下一个", + "previous": "上一个", + "live_tv": "直播电视", + "coming_soon": "即将播出", + "on_now": "正在播放", + "shows": "节目", + "movies": "电影", + "sports": "体育", + "for_kids": "儿童", + "news": "新闻" + }, + "jellyseerr": { + "confirm": "确认", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什么问题?", + "issue_type": "问题类型", + "select_an_issue": "选择一个问题", + "types": "类型", + "describe_the_issue": "(可选)描述问题...", + "submit_button": "提交", + "report_issue_button": "报告问题", + "request_button": "请求", + "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", + "failed_to_login": "登录失败", + "cast": "演员", + "details": "详情", + "status": "状态", + "original_title": "原标题", + "series_type": "剧集类型", + "release_dates": "发行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "预算", + "original_language": "原始语言", + "production_country": "制作国家/地区", + "studios": "工作室", + "network": "网络", + "currently_streaming_on": "目前在以下流媒体上播放", + "advanced": "高级设置", + "request_as": "选择用户以请求", + "tags": "标签", + "quality_profile": "质量配置文件", + "root_folder": "根文件夹", + "season_x": "第 {{seasons}} 季", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出场", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", + "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", + "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", + "issue_submitted": "问题已提交!", + "requested_item": "已请求 {{item}}!", + "you_dont_have_permission_to_request": "您无权请求媒体!", + "something_went_wrong_requesting_media": "请求媒体时出了些问题!" + } + }, + "tabs": { + "home": "主页", + "search": "搜索", + "library": "媒体库", + "custom_links": "自定义链接", + "favorites": "收藏" + } +} From fae588b0f0f47b7da67bfb9c278d85dfb07c4dbf Mon Sep 17 00:00:00 2001 From: Edmond <61924136+Marcio2536@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:06:43 +0800 Subject: [PATCH 09/93] fix: Improve Chinese (Traditional) Translation (#557) --- translations/zh-TW.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/translations/zh-TW.json b/translations/zh-TW.json index bd5bba84..bc4e9136 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -81,8 +81,8 @@ }, "media_controls": { "media_controls_title": "媒體控制", - "forward_skip_length": "前進跳過長度", - "rewind_length": "倒帶長度", + "forward_skip_length": "快進秒數", + "rewind_length": "倒帶秒數", "seconds_unit": "秒" }, "audio": { @@ -108,7 +108,7 @@ "Smart": "智能", "Always": "總是", "None": "無", - "OnlyForced": "僅強制" + "OnlyForced": "僅強制字幕" } }, "other": { @@ -142,7 +142,7 @@ "optimized_versions_server": "Optimized Version 伺服器", "save_button": "保存", "optimized_server": "Optimized Server", - "optimized": "優化", + "optimized": "已優化", "default": "默認", "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", @@ -152,7 +152,7 @@ "plugins": { "plugins_title": "插件", "jellyseerr": { - "jellyseerr_warning": "此集成處於早期階段。功能可能會有變化。", + "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", "server_url": "伺服器 URL", "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", "server_url_placeholder": "Jellyseerr URL...", @@ -410,7 +410,7 @@ "submit_button": "提交", "report_issue_button": "報告問題", "request_button": "請求", - "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的節目嗎?", + "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", "failed_to_login": "登入失敗", "cast": "演員", "details": "詳情", @@ -427,8 +427,8 @@ "studios": "工作室", "network": "網絡", "currently_streaming_on": "目前在以下流媒體上播放", - "advanced": "高級", - "request_as": "請求為", + "advanced": "高級設定", + "request_as": "選擇用戶以作請求", "tags": "標籤", "quality_profile": "質量配置文件", "root_folder": "根文件夾", @@ -438,7 +438,7 @@ "born": "出生", "appearances": "出場", "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請更新至至少 2.0.0", + "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", "issue_submitted": "問題已提交!", @@ -450,7 +450,7 @@ "tabs": { "home": "主頁", "search": "搜索", - "library": "庫", + "library": "媒體庫", "custom_links": "自定義鏈接", "favorites": "收藏" } From 6cc70dd12322dff016ca3faff57d2faad58245e2 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 22 Feb 2025 12:02:33 +0100 Subject: [PATCH 10/93] fix: type issues --- app/(auth)/(tabs)/(home)/sessions/index.tsx | 94 +++++++++------------ 1 file changed, 41 insertions(+), 53 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index 097205da..255da47d 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -147,7 +147,7 @@ const SessionCard = ({ session }: SessionCardProps) => { @@ -160,87 +160,76 @@ const SessionCard = ({ session }: SessionCardProps) => { }; interface TranscodingBadgesProps { - properties: Array; + properties: StreamProps; } -const TranscodingBadges = ({ properties = [] }: TranscodingBadgesProps) => { +const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { + const iconMap = { + bitrate: , + codec: , + videoRange: ( + + ), + resolution: , + language: , + audioChannels: , + } as const; + const icon = (val: string) => { - switch (val) { - case "bitrate": - return ; - break; - case "codec": - return ; - break; - case "videoRange": - return ( - - ); - break; - case "resolution": - return ; - break; - case "language": - return ; - break; - case "audioChannels": - return ; - break; - default: - return ; - } + return ( + iconMap[val as keyof typeof iconMap] ?? ( + + ) + ); }; - const formatVal = (key: String, val: any) => { + const formatVal = (key: string, val: any) => { switch (key) { case "bitrate": return formatBitrate(val); - break; default: return val; } }; - return Object.keys(properties) - .filter( - (key) => !(properties[key] === undefined || properties[key] === null) - ) - .map((key) => ( + return Object.entries(properties) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key]) => ( )); }; interface StreamProps { - resolution: String | null | undefined; - language: String | null | undefined; - codec: String | null | undefined; - bitrate: number | null | undefined; - videoRange: String | null | undefined; - audioChannels: String | null | undefined; + resolution?: string | null | undefined; + language?: string | null | undefined; + codec?: string | null | undefined; + bitrate?: number | null | undefined; + videoRange?: string | null | undefined; + audioChannels?: string | null | undefined; } interface TranscodingStreamViewProps { - title: String | undefined; - value: String; + title: string | undefined; + value?: string; isTranscoding: Boolean; - transcodeValue: String | undefined | null; - properties: Array; - transcodeProperties: Array; + transcodeValue?: string | undefined | null; + properties: StreamProps; + transcodeProperties?: StreamProps; } const TranscodingStreamView = ({ title, - value, isTranscoding, + properties, + transcodeProperties, + value, transcodeValue, - properties = [], - transcodeProperties = [], }: TranscodingStreamViewProps) => { return ( @@ -252,7 +241,7 @@ const TranscodingStreamView = ({ - {isTranscoding && ( + {isTranscoding && transcodeProperties ? ( <> @@ -267,7 +256,7 @@ const TranscodingStreamView = ({ - )} + ) : null} ); }; @@ -309,7 +298,6 @@ const TranscodingView = ({ session }: SessionCardProps) => { resolution: videoStreamTitle(), bitrate: videoStream?.BitRate, codec: videoStream?.Codec, - //videoRange: videoStream?.VideoRange }} transcodeProperties={{ bitrate: session.TranscodingInfo?.Bitrate, @@ -333,7 +321,7 @@ const TranscodingView = ({ session }: SessionCardProps) => { transcodeProperties={{ bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.AudioCodec, - audioChannels: session.TranscodingInfo?.AudioChannels, + audioChannels: session.TranscodingInfo?.AudioChannels?.toString(), }} isTranscoding={ isTranscoding && !session.TranscodingInfo?.IsVideoDirect From 5590c2f784087966aa66ca3e45ea7abeafbde79b Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 22 Feb 2025 12:08:58 +0100 Subject: [PATCH 11/93] fix: added season and episode + updated icon --- app/(auth)/(tabs)/(home)/sessions/index.tsx | 41 ++++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index 255da47d..8da32bce 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -109,16 +109,29 @@ const SessionCard = ({ session }: SessionCardProps) => { - {session.NowPlayingItem?.Name} - {!session.NowPlayingItem?.SeriesName && ( - - {session.NowPlayingItem?.ProductionYear} - - )} - {session.NowPlayingItem?.SeriesName && ( - - {session.NowPlayingItem?.SeriesName} - + {session.NowPlayingItem?.Type === "Episode" ? ( + <> + + {session.NowPlayingItem?.Name} + + + {`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`} + {" - "} + {session.NowPlayingItem.SeriesName} + + + ) : ( + <> + + {session.NowPlayingItem?.Name} + + + {session.NowPlayingItem?.ProductionYear} + + + {session.NowPlayingItem?.SeriesName} + + )} @@ -131,12 +144,12 @@ const SessionCard = ({ session }: SessionCardProps) => { - - + + {!session.PlayState?.IsPaused ? ( - + ) : ( - + )} From af2bd030e9b14154cecbaa97c1005bb67ac59ec5 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 22 Feb 2025 13:09:17 +0100 Subject: [PATCH 12/93] feat: focus search bar on second tab press (#558) --- app/(auth)/(tabs)/(search)/index.tsx | 27 ++++++++++++++++++++++++++- app/(auth)/(tabs)/_layout.tsx | 8 +++++++- utils/eventBus.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 utils/eventBus.ts diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 11a38636..6d1ac344 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -26,12 +26,14 @@ import React, { useEffect, useLayoutEffect, useMemo, + useRef, useState, } from "react"; import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useDebounce } from "use-debounce"; import { useTranslation } from "react-i18next"; +import { eventBus } from "@/utils/eventBus"; type SearchType = "Library" | "Discover"; @@ -120,21 +122,44 @@ export default function search() { [api, searchEngine, settings] ); + type HeaderSearchBarRef = { + focus: () => void; + blur: () => void; + setText: (text: string) => void; + clearText: () => void; + cancelSearch: () => void; + }; + + const searchBarRef = useRef(null); const navigation = useNavigation(); useLayoutEffect(() => { navigation.setOptions({ headerSearchBarOptions: { + ref: searchBarRef, placeholder: t("search.search"), onChangeText: (e: any) => { router.setParams({ q: "" }); setSearch(e.nativeEvent.text); }, hideWhenScrolling: false, - autoFocus: true, + autoFocus: false, }, }); }, [navigation]); + useEffect(() => { + const unsubscribe = eventBus.on("searchTabPressed", () => { + // Screen not actuve + if (!searchBarRef.current) return; + // Screen is active, focus search bar + searchBarRef.current?.focus(); + }); + + return () => { + unsubscribe(); + }; + }, []); + const { data: movies, isFetching: l1 } = useQuery({ queryKey: ["search", "movies", debouncedSearch], queryFn: () => diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 1b789c62..6d097240 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -20,6 +20,7 @@ import type { TabNavigationState, } from "@react-navigation/native"; import { SystemBars } from "react-native-edge-to-edge"; +import { eventBus } from "@/utils/eventBus"; export const NativeTabs = withLayoutContext< BottomTabNavigationOptions, @@ -76,6 +77,11 @@ export default function TabLayout() { }} /> ({ + tabPress: (e) => { + eventBus.emit("searchTabPressed"); + }, + })} name="(search)" options={{ title: t("tabs.search"), @@ -137,4 +143,4 @@ export default function TabLayout() { ); -} \ No newline at end of file +} diff --git a/utils/eventBus.ts b/utils/eventBus.ts new file mode 100644 index 00000000..4df6b19f --- /dev/null +++ b/utils/eventBus.ts @@ -0,0 +1,26 @@ +type Listener = (data?: T) => void; + +class EventBus { + private listeners: Record[]> = {}; + + on(event: string, callback: Listener): () => void { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + return () => this.off(event, callback); + } + + off(event: string, callback: Listener): void { + if (!this.listeners[event]) return; + this.listeners[event] = this.listeners[event].filter( + (fn) => fn !== callback + ); + } + + emit(event: string, data?: T): void { + this.listeners[event]?.forEach((callback) => callback(data)); + } +} + +export const eventBus = new EventBus(); From 81b91bbb971917604645b3c228f8023d3ce47e54 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 22 Feb 2025 13:11:37 +0100 Subject: [PATCH 13/93] chore --- app/(auth)/(tabs)/(home)/index.tsx | 4 ++-- app/(auth)/(tabs)/_layout.tsx | 5 +++++ components/settings/{SettingsIndex.tsx => HomeIndex.tsx} | 2 +- .../settings/{SettingsIndex.tv.tsx => HomeIndex.tv.tsx} | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) rename components/settings/{SettingsIndex.tsx => HomeIndex.tsx} (99%) rename components/settings/{SettingsIndex.tv.tsx => HomeIndex.tv.tsx} (99%) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 89d03d0c..dc04e43b 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -1,5 +1,5 @@ -import { SettingsIndex } from "@/components/settings/SettingsIndex"; +import { HomeIndex } from "@/components/settings/HomeIndex"; export default function page() { - return ; + return ; } diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 6d097240..6f581ae0 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -63,6 +63,11 @@ export default function TabLayout() { > ({ + tabPress: (e) => { + eventBus.emit("scrollToTop"); + }, + })} name="(home)" options={{ title: t("tabs.home"), diff --git a/components/settings/SettingsIndex.tsx b/components/settings/HomeIndex.tsx similarity index 99% rename from components/settings/SettingsIndex.tsx rename to components/settings/HomeIndex.tsx index 964bee31..44eebc27 100644 --- a/components/settings/SettingsIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -53,7 +53,7 @@ type MediaListSection = { type Section = ScrollingCollectionListSection | MediaListSection; -export const SettingsIndex = () => { +export const HomeIndex = () => { const router = useRouter(); const { t } = useTranslation(); diff --git a/components/settings/SettingsIndex.tv.tsx b/components/settings/HomeIndex.tv.tsx similarity index 99% rename from components/settings/SettingsIndex.tv.tsx rename to components/settings/HomeIndex.tv.tsx index afde6937..b7a8633c 100644 --- a/components/settings/SettingsIndex.tv.tsx +++ b/components/settings/HomeIndex.tv.tsx @@ -50,7 +50,7 @@ type MediaListSection = { type Section = ScrollingCollectionListSection | MediaListSection; -export const SettingsIndex = () => { +export const HomeIndex = () => { const router = useRouter(); const { t } = useTranslation(); From d5ac30b6d87c0d9848ab40dd36127432a15561a4 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 22 Feb 2025 13:20:52 +0100 Subject: [PATCH 14/93] feat: scroll to top on tab home press --- components/settings/HomeIndex.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 44eebc27..565d1d58 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -9,6 +9,7 @@ import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybac import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; +import { eventBus } from "@/utils/eventBus"; import { Feather, Ionicons } from "@expo/vector-icons"; import { Api } from "@jellyfin/sdk"; import { @@ -26,7 +27,7 @@ import NetInfo from "@react-native-community/netinfo"; import { QueryFunction, useQuery } from "@tanstack/react-query"; import { useNavigation, useRouter } from "expo-router"; import { useAtomValue } from "jotai"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -77,6 +78,8 @@ export const HomeIndex = () => { const insets = useSafeAreaInsets(); + const scrollViewRef = useRef(null); + const { downloadedFiles, cleanCacheDirectory } = useDownload(); useEffect(() => { const hasDownloads = downloadedFiles && downloadedFiles.length > 0; @@ -104,6 +107,16 @@ export const HomeIndex = () => { ); }, []); + useEffect(() => { + const unsubscribe = eventBus.on("scrollToTop", () => { + scrollViewRef.current?.scrollTo({ y: -152, animated: true }); + }); + + return () => { + unsubscribe(); + }; + }, []); + const checkConnection = useCallback(async () => { setLoadingRetry(true); const state = await NetInfo.fetch(); @@ -415,6 +428,8 @@ export const HomeIndex = () => { return ( Date: Sat, 22 Feb 2025 13:23:33 +0100 Subject: [PATCH 15/93] feat: scroll to top on tab home press --- components/settings/HomeIndex.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 565d1d58..9c0919f5 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -25,7 +25,12 @@ import { } from "@jellyfin/sdk/lib/utils/api"; import NetInfo from "@react-native-community/netinfo"; import { QueryFunction, useQuery } from "@tanstack/react-query"; -import { useNavigation, useRouter } from "expo-router"; +import { + useNavigation, + usePathname, + useRouter, + useSegments, +} from "expo-router"; import { useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -107,15 +112,17 @@ export const HomeIndex = () => { ); }, []); + const segments = useSegments(); useEffect(() => { const unsubscribe = eventBus.on("scrollToTop", () => { - scrollViewRef.current?.scrollTo({ y: -152, animated: true }); + if (segments[2] === "(home)") + scrollViewRef.current?.scrollTo({ y: -152, animated: true }); }); return () => { unsubscribe(); }; - }, []); + }, [segments]); const checkConnection = useCallback(async () => { setLoadingRetry(true); From 8e0e35afe385f5ba38f4a4e8905c82dc0bc3b63e Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 23 Feb 2025 14:35:00 +0100 Subject: [PATCH 16/93] fix: chromecast --- app/(auth)/(tabs)/(home)/settings.tsx | 5 +- bun.lock | 66 ++++++++-------- components/PlayButton.tsx | 14 ++-- components/settings/ChromecastSettings.tsx | 22 ++++++ utils/atoms/settings.ts | 2 + utils/jellyfin/media/getStreamUrl.ts | 6 +- utils/profiles/chromecast.ts | 42 +++++----- utils/profiles/chromecasth265.ts | 91 ++++++++++++++++++++++ 8 files changed, 185 insertions(+), 63 deletions(-) create mode 100644 components/settings/ChromecastSettings.tsx create mode 100644 utils/profiles/chromecasth265.ts diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 1818260f..4fe3b0cb 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -19,10 +19,11 @@ import { storage } from "@/utils/mmkv"; import { useNavigation, useRouter } from "expo-router"; import { t } from "i18next"; import React, { useEffect } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { ScrollView, Switch, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useAtom } from "jotai"; import { userAtom } from "@/providers/JellyfinProvider"; +import { ChromecastSettings } from "@/components/settings/ChromecastSettings"; export default function settings() { const router = useRouter(); @@ -79,6 +80,8 @@ export default function settings() { + + { diff --git a/bun.lock b/bun.lock index 74136017..b3c75406 100644 --- a/bun.lock +++ b/bun.lock @@ -386,7 +386,7 @@ "@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="], - "@expo/cli": ["@expo/cli@0.22.16", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.10", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.27", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-a8Ulbnji9kFatnOtsWGCRs6nMUj9UNC0/WhE74HQdXGDGMn5Pl8eNe3cLMy9G54DdqAmEZmRZpgXmcudT78fEQ=="], + "@expo/cli": ["@expo/cli@0.22.18", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.11", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.28", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-TWGKHWTYU9xE7YETPk2zQzLPl+bldpzZCa0Cqg0QeENpu03ZEnMxUqrgHwrbWGTf7ONTYC1tODBkFCFw/qgPGA=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], @@ -400,13 +400,13 @@ "@expo/env": ["@expo/env@0.4.2", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A=="], - "@expo/fingerprint": ["@expo/fingerprint@0.11.10", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-34ZwPjbnnD7KHSyceaxcLQbClCkYHbEp6wBDe+aqimvQw25m2LnliN1cMCVQnpOHkBFRTcbKlowby0fIxAm2bQ=="], + "@expo/fingerprint": ["@expo/fingerprint@0.11.11", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-gNyn1KnAOpEa8gSNsYqXMTcq0fSwqU/vit6fP5863vLSKxHm/dNt/gm/uZJxrRZxKq71KUJWF6I7d3z8qIfq5g=="], "@expo/image-utils": ["@expo/image-utils@0.6.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "fs-extra": "9.0.0", "getenv": "^1.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew=="], "@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="], - "@expo/metro-config": ["@expo/metro-config@0.19.10", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.9", "@expo/env": "~0.4.1", "@expo/json-file": "~9.0.1", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-/CtsMLhELJRJjAllM4EUnlPUAixn8Q2YhorKBa4uXZ6FvTEZWHJjqsXnQD39gWSEuAIVwLfJ1qgJi8666+dW2w=="], + "@expo/metro-config": ["@expo/metro-config@0.19.11", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.10", "@expo/env": "~0.4.2", "@expo/json-file": "~9.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-XaobHTcsoHQdKEH7PI/DIpr2QiugkQmPYolbfzkpSJMplNWfSh+cTRjrm4//mS2Sb78qohtu0u2CGJnFqFUGag=="], "@expo/metro-runtime": ["@expo/metro-runtime@4.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw=="], @@ -416,7 +416,7 @@ "@expo/plist": ["@expo/plist@0.2.2", "", { "dependencies": { "@xmldom/xmldom": "~0.7.7", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g=="], - "@expo/prebuild-config": ["@expo/prebuild-config@8.0.27", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-UFGOx4TfiT2gOde8RylwmXctp/WvqBQ4TN7z1YL0WWXfG9TWfO7HdsUnqQhGMW+CDDc7FOJMEo8q1a6xiikfYA=="], + "@expo/prebuild-config": ["@expo/prebuild-config@8.0.28", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SDDgCKKS1wFNNm3de2vBP8Q5bnxcabuPDE9Mnk9p7Gb4qBavhwMbAtrLcAyZB+WRb4QM+yan3z3K95vvCfI/+A=="], "@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.0", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-RILoWhREgjMdr1NUSmZa/cHg8onV2YPDAMOy0iIP1c3H7nT9QQZf5dQNHK8ehcLM82sarVxriBJyYSSHAx7j6w=="], @@ -430,7 +430,7 @@ "@expo/vector-icons": ["@expo/vector-icons@14.0.4", "", { "dependencies": { "prop-types": "^15.8.1" } }, "sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ=="], - "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.4", "", {}, "sha512-spXCVXxbeKOe8YZ9igd+MDfXZe6LeDvFAdILijeTSG+XcxGrZLmqMWWkFKR0nV8lTWZ+NugUT3CoiXmEuKKQ7w=="], + "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.5", "", {}, "sha512-Ta9KzslHAIbw2ZoyZ7Ud7/QImucy+K4YvOqo9AhGfUfH76hQzaffQreOySzYusDfW8Y+EXh0ZNWE68dfCumFFw=="], "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], @@ -676,9 +676,9 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.66.0", "", {}, "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.66.4", "", {}, "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.66.0", "", { "dependencies": { "@tanstack/query-core": "5.66.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.66.9", "", { "dependencies": { "@tanstack/query-core": "5.66.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -828,7 +828,7 @@ "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], - "babel-preset-expo": ["babel-preset-expo@12.0.8", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-bojAddWZJusLs3NVdF+jN3WweTYVEZXBKIeO0sOhqOg7UPh5w1bnMkx7SDua0FgQMGBxb13qM31Y46yeZnmXjw=="], + "babel-preset-expo": ["babel-preset-expo@12.0.9", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-1c+ysrTavT49WgVAj0OX/TEzt1kU2mfPhDaDajstshNHXFKPenMPWSViA/DHrJKVIMwaqr+z3GbUOD9GtKgpdg=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -896,7 +896,7 @@ "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001699", "", {}, "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w=="], + "caniuse-lite": ["caniuse-lite@1.0.30001700", "", {}, "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ=="], "centra": ["centra@2.7.0", "", { "dependencies": { "follow-redirects": "^1.15.6" } }, "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg=="], @@ -1056,7 +1056,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.100", "", {}, "sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.103", "", {}, "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1086,6 +1086,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -1110,11 +1112,11 @@ "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - "expo": ["expo@52.0.35", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.16", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.10", "@expo/metro-config": "0.19.10", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.8", "expo-asset": "~11.0.3", "expo-constants": "~17.0.6", "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-keep-awake": "~14.0.2", "expo-modules-autolinking": "2.0.7", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-VagwS6MJbU0Eky18i4amkkSy7FTi0v31B0W+qoEcsU4x5OurA381rxw4qGsQE+8pmSD/Gf3DGb8ygJw+HoAsXw=="], + "expo": ["expo@52.0.37", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.18", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.11", "@expo/metro-config": "0.19.11", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.9", "expo-asset": "~11.0.4", "expo-constants": "~17.0.7", "expo-file-system": "~18.0.11", "expo-font": "~13.0.4", "expo-keep-awake": "~14.0.3", "expo-modules-autolinking": "2.0.8", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-fo37ClqjNLOVInerm7BU27H8lfPfeTC7Pmu72roPzq46DnJfs+KzTxTzE34GcJ0b6hMUx9FRSSGyTQqxzo2TVQ=="], "expo-application": ["expo-application@6.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A=="], - "expo-asset": ["expo-asset@11.0.3", "", { "dependencies": { "@expo/image-utils": "^0.6.4", "expo-constants": "~17.0.5", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vgJnC82IooAVMy5PxbdFIMNJhW4hKAUyxc5VIiAPPf10vFYw6CqHm+hrehu4ST1I4bvg5PV4uKdPxliebcbgLg=="], + "expo-asset": ["expo-asset@11.0.4", "", { "dependencies": { "@expo/image-utils": "^0.6.5", "expo-constants": "~17.0.7", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CdIywU0HrR3wsW5c3n0cT3jW9hccZdnqGsRqY+EY/RWzJbDXtDfAQVEiFHO3mDK7oveUwrP2jK/6ZRNek41/sg=="], "expo-background-fetch": ["expo-background-fetch@13.0.5", "", { "dependencies": { "expo-task-manager": "~12.0.5" }, "peerDependencies": { "expo": "*" } }, "sha512-rLRM+rYDRT0fA0Oaet5ibJK3nKVRkfdjXjISHxjUvIE4ktD9pE+UjAPPdjTXZ5CkNb3JyNNhQGJEGpdJC2HLKw=="], @@ -1124,7 +1126,7 @@ "expo-build-properties": ["expo-build-properties@0.13.2", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-ML2GwBgn0Bo4yPgnSGb7h3XVxCigS/KFdid3xPC2HldEioTP3UewB/2Qa4WBsam9Fb7lAuRyVHAfRoA3swpDzg=="], - "expo-constants": ["expo-constants@17.0.6", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/env": "~0.4.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-rl3/hBIIkh4XDkCEMzGpmY6kWj2G1TA4Mq2joeyzoFBepJuGjqnGl7phf/71sTTgamQ1hmhKCLRNXMpRqzzqxw=="], + "expo-constants": ["expo-constants@17.0.7", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sp5NUiV17I3JblVPIBDgoxgt7JIZS30vcyydCYHxsEoo+aKaeRYXxGYilCvb9lgI6BBwSL24sQ6ZjWsCWoF1VA=="], "expo-crypto": ["expo-crypto@14.0.2", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ=="], @@ -1140,17 +1142,17 @@ "expo-eas-client": ["expo-eas-client@0.13.2", "", {}, "sha512-2RAAGtkO9vseoJZuW4mhJkiNQ6+FfLrX66OTMq4Qj9mRKZV2Uq/ZquxUGIeJyYqBy4vNYeKbuPd2oJtsV9LBGQ=="], - "expo-file-system": ["expo-file-system@18.0.10", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-+GnxkI+J9tOzUQMx+uIOLBEBsO2meyoYHxd87m9oT9M//BpepYqI1AvYBH8YM4dgr9HaeaeLr7z5XFVqfL8tWg=="], + "expo-file-system": ["expo-file-system@18.0.11", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-yDwYfEzWgPXsBZHJW2RJ8Q66ceiFN9Wa5D20pp3fjXVkzPBDwxnYwiPWk4pVmCa5g4X5KYMoMne1pUrsL4OEpg=="], - "expo-font": ["expo-font@13.0.3", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-9IdYz+A+b3KvuCYP7DUUXF4VMZjPU+IsvAnLSVJ2TfP6zUD2JjZFx3jeo/cxWRkYk/aLj5+53Te7elTAScNl4Q=="], + "expo-font": ["expo-font@13.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw=="], "expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="], - "expo-image": ["expo-image@2.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-FAq7uyaTAfLWER3lN+KVAtep7IfGPZN9ygnVKW4GvgnvR4hKhTtZ5WNxiJ18KKLVb4nUKuHOpQeJNnljy3dtmA=="], + "expo-image": ["expo-image@2.0.6", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-NHpIZmGnrPbyDadil6eK+sUgyFMQfapEVb7YaGgxSFWBUQ1rSpjqdIQrCD24IZTO9uSH8V+hMh2ROxrAjAixzQ=="], "expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="], - "expo-keep-awake": ["expo-keep-awake@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-71XAMnoWjKZrN8J7Q3+u0l9Ytp4OfhNAYz8BCWF1/9aFUw09J3I7Z5DuI3MUsVMa/KWi+XhG+eDUFP8cVA19Uw=="], + "expo-keep-awake": ["expo-keep-awake@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg=="], "expo-linear-gradient": ["expo-linear-gradient@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ=="], @@ -1160,7 +1162,7 @@ "expo-manifests": ["expo-manifests@0.15.6", "", { "dependencies": { "@expo/config": "~10.0.9", "expo-json-utils": "~0.14.0" }, "peerDependencies": { "expo": "*" } }, "sha512-z+TFICrijMaqBvcJkVx8WzgmOsV6ZJGvaPNQKZr4DA6uqugFMtvAQVikDjIq7SEc3n7IgPk0GR4ZN3/KnnkeVA=="], - "expo-modules-autolinking": ["expo-modules-autolinking@2.0.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-rkGc6a/90AC3q8wSy4V+iIpq6Fd0KXmQICKrvfmSWwrMgJmLfwP4QTrvLYPYOOMjFwNJcTaohcH8vzW/wYKrMg=="], + "expo-modules-autolinking": ["expo-modules-autolinking@2.0.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ=="], "expo-modules-core": ["expo-modules-core@2.2.2", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-SgjK86UD89gKAscRK3bdpn6Ojfs/KU4GujtuFx1wm4JaBjmXH4aakWkItkPlAV2pjIiHJHWQbENL9xjbw/Qr/g=="], @@ -1184,7 +1186,7 @@ "expo-task-manager": ["expo-task-manager@12.0.5", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-tDHOBYORA6wuO32NWwz/Egrvn+N6aANHAa0DFs+01VK/IJZfU9D05ZN6M5XYIlZv5ll4GSX1wJZyTCY0HZGapw=="], - "expo-updates": ["expo-updates@0.26.18", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.5", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-i9on8jMLrDxtr3Jwpmqj14oa4PWxSKYrHhJYK40xATV6qrauTija9R7BkN0hQjD4LpElt5UJW2/YUP30UsTFqA=="], + "expo-updates": ["expo-updates@0.26.19", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.6", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-h40UrG0n1nCb2na1ffz+mNQtsnr7/BxxK+EtXJSqCaD9PIGaTGe20tasmo1oVskv3s37zfv0x93+6uTjanieQg=="], "expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="], @@ -1206,7 +1208,7 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@4.5.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w=="], + "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], @@ -1238,7 +1240,7 @@ "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "flow-parser": ["flow-parser@0.261.1", "", {}, "sha512-2l5bBKeVtT+d+1CYSsTLJ+iP2FuoR7zjbDQI/v6dDRiBpx3Lb20Z/tLS37ReX/lcodyGSHC2eA/Nk63hB+mkYg=="], + "flow-parser": ["flow-parser@0.261.2", "", {}, "sha512-RtunoakA3YjtpAxPSOBVW6lmP5NYmETwkpAfNkdr8Ovf86ENkbD3mtPWnswFTIUtRvjwv0i8ZSkHK+AzsUg1JA=="], "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], @@ -1248,7 +1250,7 @@ "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], - "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], @@ -1446,7 +1448,7 @@ "join-component": ["join-component@1.1.0", "", {}, "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ=="], - "jotai": ["jotai@2.12.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-j5B4NmUw8gbuN7AG4NufWw00rfpm6hexL2CVhKD7juoP2YyD9FEUV5ar921JMvadyrxQhU1NpuKUL3QfsAlVpA=="], + "jotai": ["jotai@2.12.1", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ=="], "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], @@ -1748,7 +1750,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="], + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="], @@ -1818,7 +1820,7 @@ "react-helmet-async": ["react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="], - "react-i18next": ["react-i18next@15.4.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw=="], + "react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="], "react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="], @@ -1830,7 +1832,7 @@ "react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="], - "react-native-compressor": ["react-native-compressor@1.10.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-i51DfTwfLcKorWbTXtnPOcQC4SQDuC+DqKkSl9wF9qAUmNS9PtipYZCXOvWShYFnX0mmcWw5vwEp2b2V73PaDQ=="], + "react-native-compressor": ["react-native-compressor@1.10.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-58gbmJ+8IvsKP8JKK1E8XW5trfQY3dNuH7S0hYw0tSRQc6l0GZ3k8TYtoUbySOc1xcQSrUo51o0Chwe8x7mUTg=="], "react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="], @@ -1904,7 +1906,7 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.3", "", { "dependencies": { "process": "^0.11.10", "readable-stream": "^4.7.0" } }, "sha512-In3boYjBnbGVrLuuRu/Ath/H6h1jgk30nAsk/71tCare1dTVoe1oMBGRn5LGf0n3c1BcHwwAqpraxX4AUAP5KA=="], + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -2042,7 +2044,7 @@ "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - "stacktrace-parser": ["stacktrace-parser@0.1.10", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg=="], + "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -2068,7 +2070,7 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "strnum": ["strnum@1.0.5", "", {}, "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="], + "strnum": ["strnum@1.1.1", "", {}, "sha512-O7aCHfYCamLCctjAiaucmE+fHf2DYHkus2OKCn4Wv03sykfFtgeECn505X6K4mPl8CRNd/qurC9guq+ynoN4pw=="], "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], @@ -2192,7 +2194,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], @@ -2286,7 +2288,7 @@ "@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "@expo/cli/form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="], + "@expo/cli/form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="], "@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -2294,7 +2296,7 @@ "@expo/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@expo/cli/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "@expo/cli/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index ff8ecab4..c57ef90d 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -32,9 +32,8 @@ import Animated, { } from "react-native-reanimated"; import { Button } from "./Button"; import { SelectedOptions } from "./ItemContent"; -const chromecastProfile = !Platform.isTV - ? require("@/utils/profiles/chromecast") - : null; +import { chromecast } from "@/utils/profiles/chromecast"; +import { chromecasth265 } from "@/utils/profiles/chromecasth265"; import { useTranslation } from "react-i18next"; import { useHaptic } from "@/hooks/useHaptic"; @@ -118,12 +117,15 @@ export const PlayButton: React.FC = ({ if (state && state !== PlayServicesState.SUCCESS) { CastContext.showPlayServicesErrorDialog(state); } else { - // Get a new URL with the Chromecast device profile: + // Check if user wants H265 for Chromecast + const enableH265 = settings.enableH265ForChromecast; + + // Get a new URL with the Chromecast device profile try { const data = await getStreamUrl({ api, item, - deviceProfile: chromecastProfile, + deviceProfile: enableH265 ? chromecast : chromecasth265, startTimeTicks: item?.UserData?.PlaybackPositionTicks!, userId: user?.Id, audioStreamIndex: selectedOptions.audioIndex, @@ -132,6 +134,8 @@ export const PlayButton: React.FC = ({ subtitleStreamIndex: selectedOptions.subtitleIndex, }); + console.log("URL: ", data?.url, enableH265); + if (!data?.url) { console.warn("No URL returned from getStreamUrl", data); Alert.alert( diff --git a/components/settings/ChromecastSettings.tsx b/components/settings/ChromecastSettings.tsx new file mode 100644 index 00000000..63cddac9 --- /dev/null +++ b/components/settings/ChromecastSettings.tsx @@ -0,0 +1,22 @@ +import { Switch, View } from "react-native"; +import { ListGroup } from "../list/ListGroup"; +import { useSettings } from "@/utils/atoms/settings"; +import { ListItem } from "../list/ListItem"; + +export const ChromecastSettings: React.FC = ({ ...props }) => { + const [settings, updateSettings] = useSettings(); + return ( + + + + + updateSettings({ enableH265ForChromecast }) + } + /> + + + + ); +}; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 52ccf719..2426be4f 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -145,6 +145,7 @@ export type Settings = { safeAreaInControlsEnabled: boolean; jellyseerrServerUrl?: string; hiddenLibraries?: string[]; + enableH265ForChromecast: boolean; }; export interface Lockable { @@ -198,6 +199,7 @@ const defaultValues: Settings = { safeAreaInControlsEnabled: true, jellyseerrServerUrl: undefined, hiddenLibraries: [], + enableH265ForChromecast: false, }; const loadSettings = (): Partial => { diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 482f7833..982bb413 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -6,6 +6,7 @@ import { PlaybackInfoResponse, } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; +import { Alert } from "react-native"; export const getStreamUrl = async ({ api, @@ -80,7 +81,6 @@ export const getStreamUrl = async ({ const res2 = await getMediaInfoApi(api).getPlaybackInfo( { - userId, itemId: item.Id!, }, { @@ -148,4 +148,8 @@ export const getStreamUrl = async ({ }; } } + + Alert.alert("Error", "Could not play this item"); + + return null; }; diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index da4d0588..9e9257e4 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -1,27 +1,16 @@ import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; -export const chromecastProfile: DeviceProfile = { +export const chromecast: DeviceProfile = { Name: "Chromecast Video Profile", MaxStreamingBitrate: 8000000, // 8 Mbps MaxStaticBitrate: 8000000, // 8 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps - CodecProfiles: [ - { - Type: "Video", - Codec: "h264", - }, - { - Type: "Audio", - Codec: "aac,mp3,flac,opus,vorbis", - }, - ], - ContainerProfiles: [], DirectPlayProfiles: [ { Container: "mp4", Type: "Video", VideoCodec: "h264", - AudioCodec: "aac,mp3,opus,vorbis", + AudioCodec: "aac,mp3", }, { Container: "mp3", @@ -31,23 +20,15 @@ export const chromecastProfile: DeviceProfile = { Container: "aac", Type: "Audio", }, - { - Container: "flac", - Type: "Audio", - }, - { - Container: "wav", - Type: "Audio", - }, ], TranscodingProfiles: [ { Container: "ts", Type: "Video", - VideoCodec: "h264", AudioCodec: "aac,mp3", - Protocol: "hls", + VideoCodec: "h264", Context: "Streaming", + Protocol: "hls", MaxAudioChannels: "2", MinSegments: 2, BreakOnNonKeyFrames: true, @@ -56,10 +37,12 @@ export const chromecastProfile: DeviceProfile = { Container: "mp4", Type: "Video", VideoCodec: "h264", - AudioCodec: "aac", + AudioCodec: "aac,mp3", Protocol: "http", Context: "Streaming", MaxAudioChannels: "2", + MinSegments: 2, + BreakOnNonKeyFrames: true, }, { Container: "mp3", @@ -78,6 +61,17 @@ export const chromecastProfile: DeviceProfile = { MaxAudioChannels: "2", }, ], + ContainerProfiles: [], + CodecProfiles: [ + { + Type: "Video", + Codec: "h264", + }, + { + Type: "Audio", + Codec: "aac,mp3", + }, + ], SubtitleProfiles: [ { Format: "vtt", diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts new file mode 100644 index 00000000..b42ca9ab --- /dev/null +++ b/utils/profiles/chromecasth265.ts @@ -0,0 +1,91 @@ +import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; + +export const chromecasth265: DeviceProfile = { + Name: "Chromecast Video Profile", + MaxStreamingBitrate: 8000000, // 8 Mbps + MaxStaticBitrate: 8000000, // 8 Mbps + MusicStreamingTranscodingBitrate: 384000, // 384 kbps + CodecProfiles: [ + { + Type: "Video", + Codec: "hevc,h264", + }, + { + Type: "Audio", + Codec: "aac,mp3,flac,opus,vorbis", + }, + ], + ContainerProfiles: [], + DirectPlayProfiles: [ + { + Container: "mp4", + Type: "Video", + VideoCodec: "hevc,h264", + AudioCodec: "aac,mp3,opus,vorbis", + }, + { + Container: "mp3", + Type: "Audio", + }, + { + Container: "aac", + Type: "Audio", + }, + { + Container: "flac", + Type: "Audio", + }, + { + Container: "wav", + Type: "Audio", + }, + ], + TranscodingProfiles: [ + { + Container: "ts", + Type: "Video", + VideoCodec: "hevc,h264", + AudioCodec: "aac,mp3", + Protocol: "hls", + Context: "Streaming", + MaxAudioChannels: "2", + MinSegments: 2, + BreakOnNonKeyFrames: true, + }, + { + Container: "mp4", + Type: "Video", + VideoCodec: "h264", + AudioCodec: "aac", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + }, + { + Container: "mp3", + Type: "Audio", + AudioCodec: "mp3", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + }, + { + Container: "aac", + Type: "Audio", + AudioCodec: "aac", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + }, + ], + SubtitleProfiles: [ + { + Format: "vtt", + Method: "Encode", + }, + { + Format: "vtt", + Method: "Encode", + }, + ], +}; From 0b966d7c048c209762989e9866348938f330d116 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 23 Feb 2025 14:50:14 +0100 Subject: [PATCH 17/93] fix: add hevc to chromecast h265 profile --- utils/profiles/chromecast.ts | 39 +++++++++++++++++++------------- utils/profiles/chromecasth265.ts | 7 +++--- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index 9e9257e4..1a65ce2d 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -5,12 +5,23 @@ export const chromecast: DeviceProfile = { MaxStreamingBitrate: 8000000, // 8 Mbps MaxStaticBitrate: 8000000, // 8 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps + CodecProfiles: [ + { + Type: "Video", + Codec: "h264", + }, + { + Type: "Audio", + Codec: "aac,mp3,flac,opus,vorbis", + }, + ], + ContainerProfiles: [], DirectPlayProfiles: [ { Container: "mp4", Type: "Video", VideoCodec: "h264", - AudioCodec: "aac,mp3", + AudioCodec: "aac,mp3,opus,vorbis", }, { Container: "mp3", @@ -20,15 +31,23 @@ export const chromecast: DeviceProfile = { Container: "aac", Type: "Audio", }, + { + Container: "flac", + Type: "Audio", + }, + { + Container: "wav", + Type: "Audio", + }, ], TranscodingProfiles: [ { Container: "ts", Type: "Video", - AudioCodec: "aac,mp3", VideoCodec: "h264", - Context: "Streaming", + AudioCodec: "aac,mp3", Protocol: "hls", + Context: "Streaming", MaxAudioChannels: "2", MinSegments: 2, BreakOnNonKeyFrames: true, @@ -37,12 +56,11 @@ export const chromecast: DeviceProfile = { Container: "mp4", Type: "Video", VideoCodec: "h264", - AudioCodec: "aac,mp3", + AudioCodec: "aac", Protocol: "http", Context: "Streaming", MaxAudioChannels: "2", MinSegments: 2, - BreakOnNonKeyFrames: true, }, { Container: "mp3", @@ -61,17 +79,6 @@ export const chromecast: DeviceProfile = { MaxAudioChannels: "2", }, ], - ContainerProfiles: [], - CodecProfiles: [ - { - Type: "Video", - Codec: "h264", - }, - { - Type: "Audio", - Codec: "aac,mp3", - }, - ], SubtitleProfiles: [ { Format: "vtt", diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts index b42ca9ab..49d8f882 100644 --- a/utils/profiles/chromecasth265.ts +++ b/utils/profiles/chromecasth265.ts @@ -18,7 +18,7 @@ export const chromecasth265: DeviceProfile = { ContainerProfiles: [], DirectPlayProfiles: [ { - Container: "mp4", + Container: "mp4,mkv", Type: "Video", VideoCodec: "hevc,h264", AudioCodec: "aac,mp3,opus,vorbis", @@ -53,13 +53,14 @@ export const chromecasth265: DeviceProfile = { BreakOnNonKeyFrames: true, }, { - Container: "mp4", + Container: "mp4,mkv", Type: "Video", - VideoCodec: "h264", + VideoCodec: "hevc,h264", AudioCodec: "aac", Protocol: "http", Context: "Streaming", MaxAudioChannels: "2", + MinSegments: 2, }, { Container: "mp3", From c9517252220ba5080699e7aa8f9a6e1d9afd2163 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 23 Feb 2025 15:02:50 +0100 Subject: [PATCH 18/93] chore --- components/PlayButton.tsx | 2 +- utils/profiles/chromecast.ts | 4 ++-- utils/profiles/chromecasth265.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index c57ef90d..e616895d 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -125,7 +125,7 @@ export const PlayButton: React.FC = ({ const data = await getStreamUrl({ api, item, - deviceProfile: enableH265 ? chromecast : chromecasth265, + deviceProfile: enableH265 ? chromecasth265 : chromecast, startTimeTicks: item?.UserData?.PlaybackPositionTicks!, userId: user?.Id, audioStreamIndex: selectedOptions.audioIndex, diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index 1a65ce2d..5199dfa7 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -2,8 +2,8 @@ import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecast: DeviceProfile = { Name: "Chromecast Video Profile", - MaxStreamingBitrate: 8000000, // 8 Mbps - MaxStaticBitrate: 8000000, // 8 Mbps + MaxStreamingBitrate: 16000000, // 16 Mbps + MaxStaticBitrate: 16000000, // 16 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps CodecProfiles: [ { diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts index 49d8f882..e74827b2 100644 --- a/utils/profiles/chromecasth265.ts +++ b/utils/profiles/chromecasth265.ts @@ -2,8 +2,8 @@ import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecasth265: DeviceProfile = { Name: "Chromecast Video Profile", - MaxStreamingBitrate: 8000000, // 8 Mbps - MaxStaticBitrate: 8000000, // 8 Mbps + MaxStreamingBitrate: 16000000, // 16Mbps + MaxStaticBitrate: 16000000, // 16 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps CodecProfiles: [ { From 5bcae8153831f47162bc9b82ec7b3c0fedcf0b84 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sun, 23 Feb 2025 15:03:22 +0100 Subject: [PATCH 19/93] fix(511): fixed long named translations for the subtitle can break UI (#564) --- components/SubtitleTrackSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx index 77e26c1b..ac5051e1 100644 --- a/components/SubtitleTrackSelector.tsx +++ b/components/SubtitleTrackSelector.tsx @@ -43,7 +43,7 @@ export const SubtitleTrackSelector: React.FC = ({ - + {t("item_card.subtitles")} From 1c550b1b776c19975c460feab1bb2e68809d2a41 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sun, 23 Feb 2025 15:03:52 +0100 Subject: [PATCH 20/93] feat: Mark/unmark favorite quick action (#561) --- .../series/[id].tsx | 2 +- components/AddToFavorites.tsx | 106 ++--------------- components/ItemContent.tsx | 2 +- components/common/TouchableItemRouter.tsx | 18 +-- hooks/useFavorite.ts | 109 ++++++++++++++++++ 5 files changed, 128 insertions(+), 109 deletions(-) create mode 100644 hooks/useFavorite.ts diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx index 9a284d33..eb9b660d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -84,7 +84,7 @@ const page: React.FC = () => { allEpisodes && allEpisodes.length > 0 && ( - + {!Platform.isTV && ( <> = ({ item, type, ...props }) => { - const queryClient = useQueryClient(); - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const isFavorite = useMemo(() => { - return item.UserData?.IsFavorite; - }, [item.UserData?.IsFavorite]); - - const updateItemInQueries = (newData: Partial) => { - queryClient.setQueryData( - [type, item.Id], - (old) => { - if (!old) return old; - return { - ...old, - ...newData, - UserData: { ...old.UserData, ...newData.UserData }, - }; - } - ); - }; - - const markFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).markFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: true } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); - - const unmarkFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).unmarkFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: false } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); - +export const AddToFavorites = ({ item, ...props }) => { + const { isFavorite, toggleFavorite, _} = useFavorite(item); + return ( { - if (isFavorite) { - unmarkFavoriteMutation.mutate(); - } else { - markFavoriteMutation.mutate(); - } - }} + onPress={toggleFavorite} /> ); diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 8fb538dd..1f9077f8 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -98,7 +98,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - + )} diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index d4d53e79..44aceafe 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,4 +1,5 @@ import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; +import { useFavorite } from "@/hooks/useFavorite"; import { BaseItemDto, BaseItemPerson, @@ -7,7 +8,6 @@ import { useRouter, useSegments } from "expo-router"; import { PropsWithChildren, useCallback } from "react"; import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import { useActionSheet } from "@expo/react-native-action-sheet"; -import { useHaptic } from "@/hooks/useHaptic"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -57,14 +57,14 @@ export const TouchableItemRouter: React.FC> = ({ const segments = useSegments(); const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); - + const { isFavorite, toggleFavorite, _} = useFavorite(item); + const from = segments[2]; const showActionSheet = useCallback(() => { - if (!(item.Type === "Movie" || item.Type === "Episode")) return; - - const options = ["Mark as Played", "Mark as Not Played", "Cancel"]; - const cancelButtonIndex = 2; + if (!(item.Type === "Movie" || item.Type === "Episode" || item.Type === "Series")) return; + const options = ["Mark as Played", "Mark as Not Played", isFavorite ? "Unmark as Favorite" : "Mark as Favorite", "Cancel"]; + const cancelButtonIndex = 3; showActionSheetWithOptions( { @@ -74,14 +74,14 @@ export const TouchableItemRouter: React.FC> = ({ async (selectedIndex) => { if (selectedIndex === 0) { await markAsPlayedStatus(true); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } else if (selectedIndex === 1) { await markAsPlayedStatus(false); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else if (selectedIndex === 2) { + toggleFavorite() } } ); - }, [showActionSheetWithOptions, markAsPlayedStatus]); + }, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]); if ( from === "(home)" || diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts new file mode 100644 index 00000000..437d290e --- /dev/null +++ b/hooks/useFavorite.ts @@ -0,0 +1,109 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useEffect, useState, useMemo } from "react"; + +export const useFavorite = (item: BaseItemDto) => { + const queryClient = useQueryClient(); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const type = "item"; + const [isFavorite, setIsFavorite] = useState(item.UserData?.IsFavorite); + + useEffect(() => { + setIsFavorite(item.UserData?.IsFavorite); + }, [item.UserData?.IsFavorite]); + + const updateItemInQueries = (newData: Partial) => { + queryClient.setQueryData( + [type, item.Id], + (old) => { + if (!old) return old; + return { + ...old, + ...newData, + UserData: { ...old.UserData, ...newData.UserData }, + }; + } + ); + }; + + const markFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).markFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: true } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(true); + }, + }); + + const unmarkFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).unmarkFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: false } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(false); + }, + }); + + const toggleFavorite = () => { + if (isFavorite) { + unmarkFavoriteMutation.mutate(); + } else { + markFavoriteMutation.mutate(); + } + }; + + return { + isFavorite, + toggleFavorite, + markFavoriteMutation, + unmarkFavoriteMutation, + }; +}; From e651b975b7e8ed3832eca11d97696fb95deff089 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 23 Feb 2025 15:08:14 +0100 Subject: [PATCH 21/93] fix: ts error --- components/common/TouchableItemRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 44aceafe..db89e52d 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -57,7 +57,7 @@ export const TouchableItemRouter: React.FC> = ({ const segments = useSegments(); const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); - const { isFavorite, toggleFavorite, _} = useFavorite(item); + const { isFavorite, toggleFavorite } = useFavorite(item); const from = segments[2]; From b581a077e181d51267ed9bc150bda66d204c550c Mon Sep 17 00:00:00 2001 From: Alex <111128610+Alexk2309@users.noreply.github.com> Date: Mon, 24 Feb 2025 01:40:10 +1100 Subject: [PATCH 22/93] General refactoring (#559) --- .vscode/settings.json | 1 + app/(auth)/player/direct-player.tsx | 403 ++++---- components/PlayButton.tsx | 6 +- components/PlayButton.tv.tsx | 18 +- components/video-player/controls/Controls.tsx | 219 ++--- .../controls/contexts/VideoContext.tsx | 93 +- .../controls/dropdown/DropdownView.tsx | 72 +- modules/vlc-player/ios/VlcPlayer.podspec | 1 + modules/vlc-player/ios/VlcPlayerView.swift | 4 +- translations/it.json | 890 +++++++++--------- translations/ja.json | 2 +- translations/nl.json | 8 +- translations/zh-CN.json | 2 +- translations/zh-TW.json | 2 +- utils/profiles/native.js | 2 +- 15 files changed, 814 insertions(+), 909 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 22480b68..42b83625 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, + "prettier.printWidth": 120, "[swift]": { "editor.defaultFormatter": "sswg.swift-lang" } diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 0603626c..51d5c3dc 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -12,40 +12,26 @@ import { ProgressUpdatePayload, VlcPlayerViewRef, } from "@/modules/vlc-player/src/VlcPlayer.types"; -// import { useDownload } from "@/providers/DownloadProvider"; -const downloadProvider = !Platform.isTV - ? require("@/providers/DownloadProvider") - : null; +const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { writeToLog } from "@/utils/log"; import native from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; -import { - getPlaystateApi, - getUserLibraryApi, -} from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; +import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useHaptic } from "@/hooks/useHaptic"; import { useGlobalSearchParams, useNavigation } from "expo-router"; import { useAtomValue } from "jotai"; -import React, { - useCallback, - useMemo, - useRef, - useState, - useEffect, -} from "react"; -import { Alert, View, AppState, AppStateStatus, Platform } from "react-native"; +import React, { useCallback, useMemo, useRef, useState, useEffect } from "react"; +import { Alert, View, Platform } from "react-native"; import { useSharedValue } from "react-native-reanimated"; import { useSettings } from "@/utils/atoms/settings"; import { useTranslation } from "react-i18next"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; +import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; export default function page() { - console.log("Direct Player"); const videoRef = useRef(null); const user = useAtomValue(userAtom); const api = useAtomValue(apiAtom); @@ -93,111 +79,101 @@ export default function page() { offline: string; }>(); const [settings] = useSettings(); + const insets = useSafeAreaInsets(); const offline = offlineStr === "true"; const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined; const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1; - const bitrateValue = bitrateValueStr - ? parseInt(bitrateValueStr, 10) - : BITRATES[0].value; + const bitrateValue = bitrateValueStr ? parseInt(bitrateValueStr, 10) : BITRATES[0].value; - const { - data: item, - isLoading: isLoadingItem, - isError: isErrorItem, - } = useQuery({ - queryKey: ["item", itemId], - queryFn: async () => { - if (offline && !Platform.isTV) { - const item = await getDownloadedItem.getDownloadedItem(itemId); - if (item) return item.item; - } - - const res = await getUserLibraryApi(api!).getItem({ - itemId, - userId: user?.Id, - }); - - return res.data; - }, - enabled: !!itemId, - staleTime: 0, + const [item, setItem] = useState(null); + const [itemStatus, setItemStatus] = useState({ + isLoading: true, + isError: false, }); - const [stream, setStream] = useState<{ - mediaSource: MediaSourceInfo; - url: string; - sessionId: string | undefined; - } | null>(null); - const [isLoadingStream, setIsLoadingStream] = useState(true); - const [isErrorStream, setIsErrorStream] = useState(false); - useEffect(() => { - const fetchStream = async () => { - setIsLoadingStream(true); - setIsErrorStream(false); - + const fetchItemData = async () => { + setItemStatus({ isLoading: true, isError: false }); try { + let fetchedItem: BaseItemDto | null = null; if (offline && !Platform.isTV) { const data = await getDownloadedItem.getDownloadedItem(itemId); - if (!data?.mediaSource) { - setStream(null); - return; - } - - const url = await getDownloadedFileUrl(data.item.Id!); - - if (item) { - setStream({ - mediaSource: data.mediaSource as MediaSourceInfo, - url, - sessionId: undefined, - }); - return; - } + if (data) fetchedItem = data.item as BaseItemDto; + } else { + const res = await getUserLibraryApi(api!).getItem({ + itemId, + userId: user?.Id, + }); + fetchedItem = res.data; } - - const res = await getStreamUrl({ - api, - item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, - audioStreamIndex: audioIndex, - maxStreamingBitrate: bitrateValue, - mediaSourceId: mediaSourceId, - subtitleStreamIndex: subtitleIndex, - deviceProfile: native, - }); - - if (!res) { - setStream(null); - return; - } - - const { mediaSource, sessionId, url } = res; - - if (!sessionId || !mediaSource || !url) { - Alert.alert(t("player.error"), t("player.failed_to_get_stream_url")); - setStream(null); - return; - } - - setStream({ - mediaSource, - sessionId, - url, - }); + setItem(fetchedItem); } catch (error) { - console.error("Error fetching stream:", error); - setIsErrorStream(true); - setStream(null); + console.error("Failed to fetch item:", error); + setItemStatus({ isLoading: false, isError: true }); } finally { - setIsLoadingStream(false); + setItemStatus({ isLoading: false, isError: false }); } }; - fetchStream(); - }, [itemId, mediaSourceId]); + if (itemId) { + fetchItemData(); + } + }, [itemId, offline, api, user?.Id]); + + interface Stream { + mediaSource: MediaSourceInfo; + sessionId: string; + url: string; + } + + const [stream, setStream] = useState(null); + const [streamStatus, setStreamStatus] = useState({ + isLoading: true, + isError: false, + }); + + useEffect(() => { + const fetchStreamData = async () => { + try { + let result: Stream | null = null; + if (offline && !Platform.isTV) { + const data = await getDownloadedItem.getDownloadedItem(itemId); + if (!data?.mediaSource) return; + const url = await getDownloadedFileUrl(data.item.Id!); + if (item) { + result = { mediaSource: data.mediaSource, sessionId: "", url }; + } + } else { + const res = await getStreamUrl({ + api, + item, + startTimeTicks: item?.UserData?.PlaybackPositionTicks!, + userId: user?.Id, + audioStreamIndex: audioIndex, + maxStreamingBitrate: bitrateValue, + mediaSourceId: mediaSourceId, + subtitleStreamIndex: subtitleIndex, + deviceProfile: native, + }); + if (!res) return; + const { mediaSource, sessionId, url } = res; + if (!sessionId || !mediaSource || !url) { + Alert.alert(t("player.error"), t("player.failed_to_get_stream_url")); + return; + } + result = { mediaSource, sessionId, url }; + } + setStream(result); + } catch (error) { + console.error("Failed to fetch stream:", error); + setStreamStatus({ isLoading: false, isError: true }); + } finally { + setStreamStatus({ isLoading: false, isError: false }); + } + }; + fetchStreamData(); + }, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]); const togglePlay = useCallback(async () => { if (!api) return; @@ -208,37 +184,11 @@ export default function page() { } else { videoRef.current?.play(); } - - if (!offline && stream) { - await getPlaystateApi(api).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: msToTicks(progress.get()), - isPaused: !isPlaying, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream.sessionId, - }); - } - }, [ - isPlaying, - api, - item, - stream, - videoRef, - audioIndex, - subtitleIndex, - mediaSourceId, - offline, - progress, - ]); + }, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]); const reportPlaybackStopped = useCallback(async () => { if (offline) return; - const currentTimeInTicks = msToTicks(progress.get()); - await getPlaystateApi(api!).onPlaybackStopped({ itemId: item?.Id!, mediaSourceId: mediaSourceId, @@ -255,12 +205,18 @@ export default function page() { videoRef.current?.stop(); }, [videoRef, reportPlaybackStopped]); + useEffect(() => { + const beforeRemoveListener = navigation.addListener("beforeRemove", stop); + return () => { + beforeRemoveListener(); + }; + }, [navigation, stop]); + const onProgress = useCallback( async (data: ProgressUpdatePayload) => { if (isSeeking.get() || isPlaybackStopped) return; const { currentTime } = data.nativeEvent; - if (isBuffering) { setIsBuffering(false); } @@ -284,9 +240,57 @@ export default function page() { playSessionId: stream.sessionId, }); }, - [item?.Id, isSeeking, api, isPlaybackStopped, audioIndex, subtitleIndex] + [item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering] ); + const onPipStarted = useCallback((e: PipStartedPayload) => { + const { pipStarted } = e.nativeEvent; + setIsPipStarted(pipStarted); + }, []); + + const changePlaybackState = useCallback( + async (isPlaying: boolean) => { + if (!api || offline || !stream) return; + await getPlaystateApi(api).onPlaybackProgress({ + itemId: item?.Id!, + audioStreamIndex: audioIndex ? audioIndex : undefined, + subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, + mediaSourceId: mediaSourceId, + positionTicks: msToTicks(progress.get()), + isPaused: !isPlaying, + playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: stream.sessionId, + }); + }, + [api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress] + ); + + const startPosition = useMemo(() => { + if (offline) return 0; + return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0; + }, [item]); + + const reportPlaybackStart = useCallback(async () => { + if (offline || !stream) return; + await getPlaystateApi(api!).onPlaybackStart({ + itemId: item?.Id!, + audioStreamIndex: audioIndex ? audioIndex : undefined, + subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, + mediaSourceId: mediaSourceId, + playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: stream?.sessionId ? stream?.sessionId : undefined, + }); + hasReportedRef.current = true; + }, [api, item, stream]); + + const hasReportedRef = useRef(false); + useEffect(() => { + if (stream && !hasReportedRef.current) { + reportPlaybackStart(); + hasReportedRef.current = true; // Mark as reported + } + }, [stream]); + useWebSocket({ isPlaying: isPlaying, togglePlay: togglePlay, @@ -294,75 +298,41 @@ export default function page() { offline, }); - const onPipStarted = useCallback((e: PipStartedPayload) => { - const { pipStarted } = e.nativeEvent; - setIsPipStarted(pipStarted); - }, []); + const onPlaybackStateChanged = useCallback( + async (e: PlaybackStatePayload) => { + const { state, isBuffering, isPlaying } = e.nativeEvent; - const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => { - const { state, isBuffering, isPlaying } = e.nativeEvent; + if (state === "Playing") { + setIsPlaying(true); + await changePlaybackState(true); + if (!Platform.isTV) await activateKeepAwakeAsync(); + return; + } - if (state === "Playing") { - setIsPlaying(true); - if (!Platform.isTV) await activateKeepAwakeAsync() - return; - } + if (state === "Paused") { + setIsPlaying(false); + await changePlaybackState(false); + if (!Platform.isTV) await deactivateKeepAwake(); + return; + } - if (state === "Paused") { - setIsPlaying(false); - if (!Platform.isTV) await deactivateKeepAwake(); - return; - } - - if (isPlaying) { - setIsPlaying(true); - setIsBuffering(false); - } else if (isBuffering) { - setIsBuffering(true); - } - }, []); - - const startPosition = useMemo(() => { - if (offline) return 0; - - return item?.UserData?.PlaybackPositionTicks - ? ticksToSeconds(item.UserData.PlaybackPositionTicks) - : 0; - }, [item]); - - // Preselection of audio and subtitle tracks. - if (!settings) return null; - let initOptions = [`--sub-text-scale=${settings.subtitleSize}`]; - - const allAudio = - stream?.mediaSource.MediaStreams?.filter( - (audio) => audio.Type === "Audio" - ) || []; - const allSubs = - stream?.mediaSource.MediaStreams?.filter( - (sub) => sub.Type === "Subtitle" - ) || []; - const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream); - - const chosenSubtitleTrack = allSubs.find( - (sub) => sub.Index === subtitleIndex + if (isPlaying) { + setIsPlaying(true); + setIsBuffering(false); + } else if (isBuffering) { + setIsBuffering(true); + } + }, + [changePlaybackState] ); - const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex); - const notTranscoding = !stream?.mediaSource.TranscodingUrl; - if ( - chosenSubtitleTrack && - (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream) - ) { - const finalIndex = notTranscoding - ? allSubs.indexOf(chosenSubtitleTrack) - : textSubs.indexOf(chosenSubtitleTrack); - initOptions.push(`--sub-track=${finalIndex}`); - } + const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || []; - if (notTranscoding && chosenAudioTrack) { - initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`); - } + // Move all the external subtitles last, because vlc places them last. + const allSubs = + stream?.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle").sort( + (a, b) => Number(a.IsExternal) - Number(b.IsExternal) + ) || []; const externalSubtitles = allSubs .filter((sub: any) => sub.DeliveryMethod === "External") @@ -371,6 +341,22 @@ export default function page() { DeliveryUrl: api?.basePath + sub.DeliveryUrl, })); + const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream); + + const chosenSubtitleTrack = allSubs.find((sub) => sub.Index === subtitleIndex); + const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex); + + const notTranscoding = !stream?.mediaSource.TranscodingUrl; + let initOptions = [`--sub-text-scale=${settings.subtitleSize}`]; + if (chosenSubtitleTrack && (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)) { + const finalIndex = notTranscoding ? allSubs.indexOf(chosenSubtitleTrack) : textSubs.indexOf(chosenSubtitleTrack); + initOptions.push(`--sub-track=${finalIndex}`); + } + + if (notTranscoding && chosenAudioTrack) { + initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`); + } + const [isMounted, setIsMounted] = useState(false); // Add useEffect to handle mounting @@ -379,22 +365,15 @@ export default function page() { return () => setIsMounted(false); }, []); - const insets = useSafeAreaInsets(); - useEffect(() => { - const beforeRemoveListener = navigation.addListener("beforeRemove", stop); - return () => { - beforeRemoveListener(); - }; - }, [navigation]); - - if (!item || isLoadingItem || !stream) + if (itemStatus.isLoading || streamStatus.isLoading) { return ( ); + } - if (isErrorItem || isErrorStream) + if (!item || !stream || itemStatus.isError || streamStatus.isError) return ( {t("player.error")} @@ -435,10 +414,7 @@ export default function page() { }} onVideoError={(e) => { console.error("Video Error:", e.nativeEvent); - Alert.alert( - t("player.error"), - t("player.an_error_occured_while_playing_the_video") - ); + Alert.alert(t("player.error"), t("player.an_error_occured_while_playing_the_video")); writeToLog("ERROR", "Video Error", e.nativeEvent); }} /> @@ -470,7 +446,6 @@ export default function page() { setSubtitleTrack={videoRef.current.setSubtitleTrack} setSubtitleURL={videoRef.current.setSubtitleURL} setAudioTrack={videoRef.current.setAudioTrack} - stop={stop} isVlc /> ) : null} diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index e616895d..83af9a75 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -71,7 +71,7 @@ export const PlayButton: React.FC = ({ const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( - (q: string, bitrateValue: number | undefined) => { + (q: string) => { router.push(`/player/direct-player?${q}`); }, [router] @@ -94,7 +94,7 @@ export const PlayButton: React.FC = ({ const queryString = queryParams.toString(); if (!client) { - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); return; } @@ -217,7 +217,7 @@ export const PlayButton: React.FC = ({ }); break; case 1: - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); break; case cancelButtonIndex: break; diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 1fb0563c..50be8c13 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500; const MIN_PLAYBACK_WIDTH = 15; export const PlayButton: React.FC = ({ - item, - selectedOptions, - ...props -}: Props) => { + item, + selectedOptions, + ...props + }: Props) => { const { showActionSheetWithOptions } = useActionSheet(); const { t } = useTranslation(); @@ -57,7 +57,7 @@ export const PlayButton: React.FC = ({ const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( - (q: string, bitrateValue: number | undefined) => { + (q: string) => { router.push(`/player/direct-player?${q}`); }, [router] @@ -78,7 +78,7 @@ export const PlayButton: React.FC = ({ }); const queryString = queryParams.toString(); - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); return; }; @@ -88,9 +88,9 @@ export const PlayButton: React.FC = ({ if (userData && userData.PlaybackPositionTicks) { return userData.PlaybackPositionTicks > 0 ? Math.max( - (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH - ) + (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, + MIN_PLAYBACK_WIDTH + ) : 0; } return 0; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 17bd2028..2974affd 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -87,40 +87,38 @@ interface Props { setSubtitleURL?: (url: string, customName: string) => void; setSubtitleTrack?: (index: number) => void; setAudioTrack?: (index: number) => void; - stop: (() => Promise) | (() => void); isVlc?: boolean; } const CONTROLS_TIMEOUT = 4000; export const Controls: React.FC = ({ - item, - seek, - startPictureInPicture, - play, - pause, - togglePlay, - isPlaying, - isSeeking, - progress, - isBuffering, - cacheProgress, - showControls, - setShowControls, - ignoreSafeAreas, - setIgnoreSafeAreas, - mediaSource, - isVideoLoaded, - getAudioTracks, - getSubtitleTracks, - setSubtitleURL, - setSubtitleTrack, - setAudioTrack, - stop, - offline = false, - enableTrickplay = true, - isVlc = false, -}) => { + item, + seek, + startPictureInPicture, + play, + pause, + togglePlay, + isPlaying, + isSeeking, + progress, + isBuffering, + cacheProgress, + showControls, + setShowControls, + ignoreSafeAreas, + setIgnoreSafeAreas, + mediaSource, + isVideoLoaded, + getAudioTracks, + getSubtitleTracks, + setSubtitleURL, + setSubtitleTrack, + setAudioTrack, + offline = false, + enableTrickplay = true, + isVlc = false, + }) => { const [settings] = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); @@ -189,75 +187,60 @@ export const Controls: React.FC = ({ isVlc ); + const goToItemCommon = useCallback( + (item: BaseItemDto) => { + if (!item || !settings) return; + + lightHapticFeedback(); + + const previousIndexes = { + subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, + audioIndex: audioIndex ? parseInt(audioIndex) : undefined, + }; + + const { + mediaSource: newMediaSource, + audioIndex: defaultAudioIndex, + subtitleIndex: defaultSubtitleIndex, + } = getDefaultPlaySettings( + item, + settings, + previousIndexes, + mediaSource ?? undefined + ); + + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", + audioIndex: defaultAudioIndex?.toString() ?? "", + subtitleIndex: defaultSubtitleIndex?.toString() ?? "", + mediaSourceId: newMediaSource?.Id ?? "", + bitrateValue: bitrateValue.toString(), + }).toString(); + + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + }, + [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router] + ); + const goToPreviousItem = useCallback(() => { - if (!previousItem || !settings) return; - - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - previousItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: previousItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - }, [previousItem, settings, subtitleIndex, audioIndex]); + if (!previousItem) return; + goToItemCommon(previousItem); + }, [previousItem, goToItemCommon]); const goToNextItem = useCallback(() => { - if (!nextItem || !settings) return; + if (!nextItem) return; + goToItemCommon(nextItem); + }, [nextItem, goToItemCommon]); - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - nextItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: nextItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - }, [nextItem, settings, subtitleIndex, audioIndex]); + const goToItem = useCallback( + async (itemId: string) => { + const gotoItem = await getItemById(api, itemId); + if (!gotoItem) return; + goToItemCommon(gotoItem); + }, + [goToItemCommon, api] + ); const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { @@ -381,49 +364,6 @@ export const Controls: React.FC = ({ } }, [settings, isPlaying, isVlc]); - const goToItem = useCallback( - async (itemId: string) => { - try { - const gotoItem = await getItemById(api, itemId); - if (!settings || !gotoItem) return; - - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - gotoItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: gotoItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - } catch (error) { - console.error("Error in gotoEpisode:", error); - } - }, - [settings, subtitleIndex, audioIndex] - ); - const toggleIgnoreSafeAreas = useCallback(() => { setIgnoreSafeAreas((prev) => !prev); lightHapticFeedback(); @@ -497,7 +437,6 @@ export const Controls: React.FC = ({ }, [trickPlayUrl, trickplayInfo, time]); const onClose = async () => { - stop(); lightHapticFeedback(); await ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.PORTRAIT_UP @@ -549,7 +488,7 @@ export const Controls: React.FC = ({ setSubtitleTrack={setSubtitleTrack} setSubtitleURL={setSubtitleURL} > - + )} @@ -790,8 +729,8 @@ export const Controls: React.FC = ({ !nextItem ? false : isVlc - ? remainingTime < 10000 - : remainingTime < 10 + ? remainingTime < 10000 + : remainingTime < 10 } onFinish={goToNextItem} onPress={goToNextItem} diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index 7a4e5161..5b42ba01 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -1,16 +1,5 @@ import { TrackInfo } from "@/modules/vlc-player"; -import { - BaseItemDto, - MediaSourceInfo, -} from "@jellyfin/sdk/lib/generated-client"; -import React, { - createContext, - useContext, - useState, - ReactNode, - useEffect, - useMemo, -} from "react"; +import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react"; import { useControlContext } from "./ControlContext"; import { Track } from "../types"; import { router, useLocalSearchParams } from "expo-router"; @@ -27,14 +16,8 @@ const VideoContext = createContext(undefined); interface VideoProviderProps { children: ReactNode; - getAudioTracks: - | (() => Promise) - | (() => TrackInfo[]) - | undefined; - getSubtitleTracks: - | (() => Promise) - | (() => TrackInfo[]) - | undefined; + getAudioTracks: (() => Promise) | (() => TrackInfo[]) | undefined; + getSubtitleTracks: (() => Promise) | (() => TrackInfo[]) | undefined; setAudioTrack: ((index: number) => void) | undefined; setSubtitleTrack: ((index: number) => void) | undefined; setSubtitleURL: ((url: string, customName: string) => void) | undefined; @@ -55,23 +38,19 @@ export const VideoProvider: React.FC = ({ const isVideoLoaded = ControlContext?.isVideoLoaded; const mediaSource = ControlContext?.mediaSource; - const allSubs = - mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; + const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; - const { itemId, audioIndex, bitrateValue, subtitleIndex } = - useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); + const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{ + itemId: string; + audioIndex: string; + subtitleIndex: string; + mediaSourceId: string; + bitrateValue: string; + }>(); const onTextBasedSubtitle = useMemo( () => - allSubs.find( - (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream - ) || subtitleIndex === "-1", + allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1", [allSubs, subtitleIndex] ); @@ -95,21 +74,14 @@ export const VideoProvider: React.FC = ({ router.replace(`player/direct-player?${queryParams}`); }; - const setTrackParams = ( - type: "audio" | "subtitle", - index: number, - serverIndex: number - ) => { + const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => { const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack; const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex"; // If we're transcoding and we're going from a image based subtitle // to a text based subtitle, we need to change the player params. - const shouldChangePlayerParams = - type === "subtitle" && - mediaSource?.TranscodingUrl && - !onTextBasedSubtitle; + const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle; console.log("Set player params", index, serverIndex); if (shouldChangePlayerParams) { @@ -129,23 +101,22 @@ export const VideoProvider: React.FC = ({ if (getSubtitleTracks) { const subtitleData = await getSubtitleTracks(); + // Step 1: Move external subs to the end, because VLC puts external subs at the end + const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)); + + // Step 2: Apply VLC indexing logic let textSubIndex = 0; - const subtitles: Track[] = allSubs?.map((sub) => { + const processedSubs: Track[] = sortedSubs?.map((sub) => { // Always increment for non-transcoding subtitles // Only increment for text-based subtitles when transcoding - const shouldIncrement = - !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; - - const displayTitle = sub.DisplayTitle || "Undefined Subtitle"; + const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1; - const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1; if (shouldIncrement) textSubIndex++; return { - name: displayTitle, + name: sub.DisplayTitle || "Undefined Subtitle", index: sub.Index ?? -1, - originalIndex: finalIndex, setTrack: () => shouldIncrement ? setTrackParams("subtitle", finalIndex, sub.Index ?? -1) @@ -155,6 +126,9 @@ export const VideoProvider: React.FC = ({ }; }); + // Step 3: Restore the original order + const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index); + // Add a "Disable Subtitles" option subtitles.unshift({ name: "Disable", @@ -164,36 +138,25 @@ export const VideoProvider: React.FC = ({ ? setTrackParams("subtitle", -1, -1) : setPlayerParams({ chosenSubtitleIndex: "-1" }), }); - setSubtitleTracks(subtitles); } - if ( - getAudioTracks && - (audioTracks === null || audioTracks.length === 0) - ) { + if (getAudioTracks) { const audioData = await getAudioTracks(); - if (!audioData) return; - - console.log("audioData", audioData); - - const allAudio = - mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; + const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; const audioTracks: Track[] = allAudio?.map((audio, idx) => { if (!mediaSource?.TranscodingUrl) { const vlcIndex = audioData?.at(idx)?.index ?? -1; return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => - setTrackParams("audio", vlcIndex, audio.Index ?? -1), + setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1), }; } return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => - setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), + setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), }; }); setAudioTracks(audioTracks); diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index 0ee51dc1..ed329659 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,23 +1,20 @@ -import React from "react"; +import React, { useCallback } from "react"; import { TouchableOpacity, Platform } from "react-native"; import { Ionicons } from "@expo/vector-icons"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; import { useVideoContext } from "../contexts/VideoContext"; -import { useLocalSearchParams } from "expo-router"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { BITRATES } from "@/components/BitrateSelector"; +import { useControlContext } from "../contexts/ControlContext"; -interface DropdownViewProps { - showControls: boolean; - offline?: boolean; // used to disable external subs for downloads -} - -const DropdownView: React.FC = ({ - showControls, - offline = false, -}) => { +const DropdownView = () => { const videoContext = useVideoContext(); const { subtitleTracks, audioTracks } = videoContext; + const ControlContext = useControlContext(); + const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource]; + const router = useRouter(); - const { subtitleIndex, audioIndex } = useLocalSearchParams<{ + const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{ itemId: string; audioIndex: string; subtitleIndex: string; @@ -25,6 +22,21 @@ const DropdownView: React.FC = ({ bitrateValue: string; }>(); + const changeBitrate = useCallback( + (bitrate: string) => { + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", + audioIndex: audioIndex?.toString() ?? "", + subtitleIndex: subtitleIndex.toString() ?? "", + mediaSourceId: mediaSource?.Id ?? "", + bitrateValue: bitrate.toString(), + }).toString(); + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + }, + [item, mediaSource, subtitleIndex, audioIndex] + ); + return ( @@ -42,9 +54,27 @@ const DropdownView: React.FC = ({ sideOffset={8} > - - Subtitle - + Quality + + {BITRATES?.map((bitrate, idx: number) => ( + changeBitrate(bitrate.value?.toString() ?? "")} + > + {bitrate.key} + + ))} + + + + Subtitle = ({ value={subtitleIndex === sub.index.toString()} onValueChange={() => sub.setTrack()} > - - {sub.name} - + {sub.name} ))} - - Audio - + Audio = ({ value={audioIndex === track.index.toString()} onValueChange={() => track.setTrack()} > - - {track.name} - + {track.name} ))} diff --git a/modules/vlc-player/ios/VlcPlayer.podspec b/modules/vlc-player/ios/VlcPlayer.podspec index 25f2d73e..89e84814 100644 --- a/modules/vlc-player/ios/VlcPlayer.podspec +++ b/modules/vlc-player/ios/VlcPlayer.podspec @@ -12,6 +12,7 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.ios.dependency 'VLCKit', s.version s.tvos.dependency 'VLCKit', s.version + s.dependency 'Alamofire', '~> 5.10' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index 1c6dd9ea..e2614fa6 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -459,7 +459,9 @@ extension VlcPlayerView: SimpleAppLifecycleListener { } // Current solution to fixing black screen when re-entering application - if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, !self.vlc.isMediaPlaying() { + if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, + !self.vlc.isMediaPlaying() + { videoTrack.isSelected = false videoTrack.isSelectedExclusively = true self.vlc.player.play() diff --git a/translations/it.json b/translations/it.json index 314fc233..c9326a7d 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,458 +1,458 @@ { - "login": { - "username_required": "Nome utente è obbligatorio", - "error_title": "Errore", - "login_title": "Accesso", - "login_to_title": "Accedi a", - "username_placeholder": "Nome utente", - "password_placeholder": "Password", - "login_button": "Accedi", - "quick_connect": "Connessione Rapida", - "enter_code_to_login": "Inserire {{code}} per accedere", - "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", - "got_it": "Capito", - "connection_failed": "Connessione fallita", - "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", - "an_unexpected_error_occured": "Si è verificato un errore inaspettato", - "change_server": "Cambiare il server", - "invalid_username_or_password": "Nome utente o password non validi", - "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", - "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", - "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", - "there_is_a_server_error": "Si è verificato un errore del server", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" + "login": { + "username_required": "Nome utente è obbligatorio", + "error_title": "Errore", + "login_title": "Accesso", + "login_to_title": "Accedi a", + "username_placeholder": "Nome utente", + "password_placeholder": "Password", + "login_button": "Accedi", + "quick_connect": "Connessione Rapida", + "enter_code_to_login": "Inserire {{code}} per accedere", + "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", + "got_it": "Capito", + "connection_failed": "Connessione fallita", + "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", + "an_unexpected_error_occured": "Si è verificato un errore inaspettato", + "change_server": "Cambiare il server", + "invalid_username_or_password": "Nome utente o password non validi", + "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", + "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", + "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", + "there_is_a_server_error": "Si è verificato un errore del server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" + }, + "server": { + "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", + "server_url_placeholder": "http(s)://tuo-server.com", + "connect_button": "Connetti", + "previous_servers": "server precedente", + "clear_button": "Cancella", + "search_for_local_servers": "Ricerca dei server locali", + "searching": "Cercando...", + "servers": "Servers" + }, + "home": { + "no_internet": "Nessun Internet", + "no_items": "Nessun oggetto", + "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", + "go_to_downloads": "Vai agli elementi scaricati", + "oops": "Oops!", + "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", + "continue_watching": "Continua a guardare", + "next_up": "Prossimo", + "recently_added_in": "Aggiunti di recente a {{libraryName}}", + "suggested_movies": "Film consigliati", + "suggested_episodes": "Episodi consigliati", + "intro": { + "welcome_to_streamyfin": "Benvenuto a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", + "features_title": "Funzioni", + "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", + "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", + "downloads_feature_title": "Scaricamento", + "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", + "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", + "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", + "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", + "done_button": "Fatto", + "go_to_settings_button": "Vai alle impostazioni", + "read_more": "Leggi di più" }, - "server": { - "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", - "server_url_placeholder": "http(s)://tuo-server.com", - "connect_button": "Connetti", - "previous_servers": "server precedente", - "clear_button": "Cancella", - "search_for_local_servers": "Ricerca dei server locali", - "searching": "Cercando...", - "servers": "Servers" - }, - "home": { - "no_internet": "Nessun Internet", - "no_items": "Nessun oggetto", - "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", - "go_to_downloads": "Vai agli elementi scaricati", - "oops": "Oops!", - "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", - "continue_watching": "Continua a guardare", - "next_up": "Prossimo", - "recently_added_in": "Aggiunti di recente a {{libraryName}}", - "suggested_movies": "Film consigliati", - "suggested_episodes": "Episodi consigliati", - "intro": { - "welcome_to_streamyfin": "Benvenuto a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", - "features_title": "Funzioni", - "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", - "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", - "downloads_feature_title": "Scaricamento", - "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", - "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", - "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", - "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", - "done_button": "Fatto", - "go_to_settings_button": "Vai alle impostazioni", - "read_more": "Leggi di più" + "settings": { + "settings_title": "Impostazioni", + "log_out_button": "Esci", + "user_info": { + "user_info_title": "Info utente", + "user": "Utente", + "server": "Server", + "token": "Token", + "app_version": "Versione dell'App" }, - "settings": { - "settings_title": "Impostazioni", - "log_out_button": "Esci", - "user_info": { - "user_info_title": "Info utente", - "user": "Utente", - "server": "Server", - "token": "Token", - "app_version": "Versione dell'App" - }, - "quick_connect": { - "quick_connect_title": "Connessione Rapida", - "authorize_button": "Autorizza Connessione Rapida", - "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", - "success": "Successo", - "quick_connect_autorized": "Connessione Rapida autorizzata", - "error": "Errore", - "invalid_code": "Codice invalido", - "authorize": "Autorizza" - }, - "media_controls": { - "media_controls_title": "Controlli multimediali", - "forward_skip_length": "Lunghezza del salto in avanti", - "rewind_length": "Lunghezza del riavvolgimento", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Imposta la traccia audio dall'elemento precedente", - "audio_language": "Lingua Audio", - "audio_hint": "Scegli la lingua audio predefinita.", - "none": "Nessuno", - "language": "Lingua" - }, - "subtitles": { - "subtitle_title": "Sottotitoli", - "subtitle_language": "Lingua dei sottotitoli", - "subtitle_mode": "Modalità dei sottotitoli", - "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", - "subtitle_size": "Dimensione dei sottotitoli", - "subtitle_hint": "Configura la preferenza dei sottotitoli.", - "none": "Nessuno", - "language": "Lingua", - "loading": "Caricamento", - "modes": { - "Default": "Predefinito", - "Smart": "Intelligente", - "Always": "Sempre", - "None": "Nessuno", - "OnlyForced": "Solo forzati" - } - }, - "other": { - "other_title": "Altro", - "auto_rotate": "Rotazione automatica", - "video_orientation": "Orientamento del video", - "orientation": "Orientamento", - "orientations": { - "DEFAULT": "Predefinito", - "ALL": "Tutto", - "PORTRAIT": "Verticale", - "PORTRAIT_UP": "Verticale sopra", - "PORTRAIT_DOWN": "Verticale sotto", - "LANDSCAPE": "Orizzontale", - "LANDSCAPE_LEFT": "Orizzontale sinitra", - "LANDSCAPE_RIGHT": "Orizzontale destra", - "OTHER": "Altro", - "UNKNOWN": "Sconosciuto" - }, - "safe_area_in_controls": "Area sicura per i controlli", - "show_custom_menu_links": "Mostra i link del menu personalizzato", - "hide_libraries": "Nascondi Librerie", - "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", - "disable_haptic_feedback": "Disabilita il feedback aptico", - "default_quality": "Qualità predefinita" - }, - "downloads": { - "downloads_title": "Scaricamento", - "download_method": "Metodo per lo scaricamento", - "remux_max_download": "Numero di Remux da scaricare al massimo", - "auto_download": "Scaricamento automatico", - "optimized_versions_server": "Versioni del server di ottimizzazione", - "save_button": "Salva", - "optimized_server": "Server di ottimizzazione", - "optimized": "Ottimizzato", - "default": "Predefinito", - "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", - "url":"URL", - "server_url_placeholder": "http(s)://dominio.org:porta" - }, - "plugins": { - "plugins_title": "Plugin", - "jellyseerr": { - "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", - "server_url": "URL del Server", - "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", - "server_url_placeholder": "URL di Jellyseerr...", - "password": "Password", - "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", - "save_button": "Salva", - "clear_button": "Cancella", - "login_button": "Accedi", - "total_media_requests": "Totale di richieste di media", - "movie_quota_limit": "Limite di quota per i film", - "movie_quota_days": "Giorni di quota per i film", - "tv_quota_limit": "Limite di quota per le serie TV", - "tv_quota_days": "Giorni di quota per le serie TV", - "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", - "unlimited": "Illimitato" - }, - "marlin_search": { - "enable_marlin_search": "Abilita la ricerca Marlin ", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta", - "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_marlin": "Leggi di più su Marlin.", - "save_button": "Salva", - "toasts": { - "saved": "Salvato" - } - } - }, - "storage": { - "storage_title": "Spazio", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} di {{total}} usato", - "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" - }, - "intro": { - "show_intro": "Mostra intro", - "reset_intro": "Ripristina intro" - }, - "logs": { - "logs_title": "Log", - "no_logs_available": "Nessun log disponibile", - "delete_all_logs": "Cancella tutti i log" - }, - "languages": { - "title": "Lingue", - "app_language": "Lingua dell'App", - "app_language_description": "Selezione la lingua dell'app.", - "system": "Sistema" - }, - "toasts":{ - "error_deleting_files": "Errore nella cancellazione dei file", - "background_downloads_enabled": "Scaricamento in background abilitato", - "background_downloads_disabled": "Scaricamento in background disabilitato", - "connected": "Connesso", - "could_not_connect": "Non è stato possibile connettersi", - "invalid_url": "URL invalido" + "quick_connect": { + "quick_connect_title": "Connessione Rapida", + "authorize_button": "Autorizza Connessione Rapida", + "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", + "success": "Successo", + "quick_connect_autorized": "Connessione Rapida autorizzata", + "error": "Errore", + "invalid_code": "Codice invalido", + "authorize": "Autorizza" + }, + "media_controls": { + "media_controls_title": "Controlli multimediali", + "forward_skip_length": "Lunghezza del salto in avanti", + "rewind_length": "Lunghezza del riavvolgimento", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Imposta la traccia audio dall'elemento precedente", + "audio_language": "Lingua Audio", + "audio_hint": "Scegli la lingua audio predefinita.", + "none": "Nessuno", + "language": "Lingua" + }, + "subtitles": { + "subtitle_title": "Sottotitoli", + "subtitle_language": "Lingua dei sottotitoli", + "subtitle_mode": "Modalità dei sottotitoli", + "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", + "subtitle_size": "Dimensione dei sottotitoli", + "subtitle_hint": "Configura la preferenza dei sottotitoli.", + "none": "Nessuno", + "language": "Lingua", + "loading": "Caricamento", + "modes": { + "Default": "Predefinito", + "Smart": "Intelligente", + "Always": "Sempre", + "None": "Nessuno", + "OnlyForced": "Solo forzati" } }, + "other": { + "other_title": "Altro", + "auto_rotate": "Rotazione automatica", + "video_orientation": "Orientamento del video", + "orientation": "Orientamento", + "orientations": { + "DEFAULT": "Predefinito", + "ALL": "Tutto", + "PORTRAIT": "Verticale", + "PORTRAIT_UP": "Verticale sopra", + "PORTRAIT_DOWN": "Verticale sotto", + "LANDSCAPE": "Orizzontale", + "LANDSCAPE_LEFT": "Orizzontale sinitra", + "LANDSCAPE_RIGHT": "Orizzontale destra", + "OTHER": "Altro", + "UNKNOWN": "Sconosciuto" + }, + "safe_area_in_controls": "Area sicura per i controlli", + "show_custom_menu_links": "Mostra i link del menu personalizzato", + "hide_libraries": "Nascondi Librerie", + "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", + "disable_haptic_feedback": "Disabilita il feedback aptico", + "default_quality": "Qualità predefinita" + }, "downloads": { - "downloads_title": "Scaricati", - "tvseries": "Serie TV", - "movies": "Film", - "queue": "Coda", - "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", - "no_items_in_queue": "Nessun elemento in coda", - "no_downloaded_items": "Nessun elemento scaricato", - "delete_all_movies_button": "Cancella tutti i film", - "delete_all_tvseries_button": "Cancella tutte le serie TV", - "delete_all_button": "Cancella tutti", - "active_download": "Scaricamento in corso", - "no_active_downloads": "Nessun scaricamento in corso", - "active_downloads": "Scaricamenti in corso", - "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", - "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", - "back": "Indietro", - "delete": "Cancella", - "something_went_wrong": "Qualcosa è andato storto", - "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Metodi", - "toasts": { - "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", - "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", - "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", - "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", - "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", - "download_cancelled": "Scaricamento annullato", - "could_not_cancel_download": "Impossibile annullare lo scaricamento", - "download_completed": "Scaricamento completato", - "download_started_for": "Scaricamento iniziato per {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", - "download_stated_for_item": "Scaricamento iniziato per {{item}}", - "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", - "download_completed_for_item": "Scaricamento completato per {{item}}", - "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", - "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", - "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", - "go_to_downloads": "Vai agli elementi scaricati" + "downloads_title": "Scaricamento", + "download_method": "Metodo per lo scaricamento", + "remux_max_download": "Numero di Remux da scaricare al massimo", + "auto_download": "Scaricamento automatico", + "optimized_versions_server": "Versioni del server di ottimizzazione", + "save_button": "Salva", + "optimized_server": "Server di ottimizzazione", + "optimized": "Ottimizzato", + "default": "Predefinito", + "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta" + }, + "plugins": { + "plugins_title": "Plugin", + "jellyseerr": { + "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", + "server_url": "URL del Server", + "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", + "server_url_placeholder": "URL di Jellyseerr...", + "password": "Password", + "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", + "save_button": "Salva", + "clear_button": "Cancella", + "login_button": "Accedi", + "total_media_requests": "Totale di richieste di media", + "movie_quota_limit": "Limite di quota per i film", + "movie_quota_days": "Giorni di quota per i film", + "tv_quota_limit": "Limite di quota per le serie TV", + "tv_quota_days": "Giorni di quota per le serie TV", + "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", + "unlimited": "Illimitato" + }, + "marlin_search": { + "enable_marlin_search": "Abilita la ricerca Marlin ", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta", + "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_marlin": "Leggi di più su Marlin.", + "save_button": "Salva", + "toasts": { + "saved": "Salvato" + } } - } - }, - "search": { - "search_here": "Cerca qui...", - "search": "Cerca...", - "x_items": "{{count}} elementi", - "library": "Libreria", - "discover": "Scopri", - "no_results": "Nessun risultato", - "no_results_found_for": "Nessun risultato trovato per", - "movies": "Film", - "series": "Serie", - "episodes": "Episodi", - "collections": "Collezioni", - "actors": "Attori", - "request_movies": "Film Richiesti", - "request_series": "Serie Richieste", - "recently_added": "Aggiunti di Recente", - "recent_requests": "Richiesti di Recente", - "plex_watchlist": "Plex Watchlist", - "trending": "In tendenza", - "popular_movies": "Film Popolari", - "movie_genres": "Generi Film", - "upcoming_movies": "Film in arrivo", - "studios": "Studio", - "popular_tv": "Serie Popolari", - "tv_genres": "Generi Televisivi", - "upcoming_tv": "Serie in Arrivo", - "networks": "Network", - "tmdb_movie_keyword": "TMDB Parola chiave del film", - "tmdb_movie_genre": "TMDB Genere Film", - "tmdb_tv_keyword": "TMDB Parola chiave della serie", - "tmdb_tv_genre": "TMDB Genere Televisivo", - "tmdb_search": "TMDB Cerca", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", - "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" - }, - "library": { - "no_items_found": "Nessun elemento trovato", - "no_results": "Nessun risultato", - "no_libraries_found": "Nessuna libreria trovata", - "item_types": { - "movies": "film", - "series": "serie TV", - "boxsets": "cofanetti", - "items": "elementi" }, - "options": { - "display": "Display", - "row": "Fila", - "list": "Lista", - "image_style": "Stile dell'immagine", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Mostra titoli", - "show_stats": "Mostra statistiche" + "storage": { + "storage_title": "Spazio", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} di {{total}} usato", + "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" + }, + "intro": { + "show_intro": "Mostra intro", + "reset_intro": "Ripristina intro" + }, + "logs": { + "logs_title": "Log", + "no_logs_available": "Nessun log disponibile", + "delete_all_logs": "Cancella tutti i log" + }, + "languages": { + "title": "Lingue", + "app_language": "Lingua dell'App", + "app_language_description": "Selezione la lingua dell'app.", + "system": "Sistema" }, - "filters": { - "genres": "Generi", - "years": "Anni", - "sort_by": "Ordina per", - "sort_order": "Criterio di ordinamento", - "tags": "Tag" - } - }, - "favorites": { - "series": "Serie TV", - "movies": "Film", - "episodes": "Episodi", - "videos": "Video", - "boxsets": "Boxset", - "playlists": "Playlist" - }, - "custom_links": { - "no_links": "Nessun link" - }, - "player": { - "error": "Errore", - "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", - "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", - "client_error": "Errore del client", - "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", - "message_from_server": "Messaggio dal server: {{messagge}}", - "video_has_finished_playing": "La riproduzione del video è terminata!", - "no_video_source": "Nessuna sorgente video...", - "next_episode": "Prossimo Episodio", - "refresh_tracks": "Aggiorna tracce", - "subtitle_tracks": "Tracce di sottotitoli:", - "audio_tracks": "Tracce audio:", - "playback_state": "Stato della riproduzione:", - "no_data_available": "Nessun dato disponibile", - "index": "Indice:" - }, - "item_card": { - "next_up": "Il prossimo", - "no_items_to_display": "Nessun elemento da visualizzare", - "cast_and_crew": "Cast e Equipaggio", - "series": "Serie", - "seasons": "Stagioni", - "season": "Stagione", - "no_episodes_for_this_season": "Nessun episodio per questa stagione", - "overview": "Panoramica", - "more_with": "Altri con {{name}}", - "similar_items": "Elementi simili", - "no_similar_items_found": "Non sono stati trovati elementi simili", - "video": "Video", - "more_details": "Più dettagli", - "quality": "Qualità", - "audio": "Audio", - "subtitles": "Sottotitoli", - "show_more": "Mostra di più", - "show_less": "Mostra di meno", - "appeared_in": "Apparso in", - "could_not_load_item": "Impossibile caricare l'elemento", - "none": "Nessuno", - "download": { - "download_season": "Scarica Stagione", - "download_series": "Scarica Serie", - "download_episode": "Scarica Episodio", - "download_movie": "Scarica Film", - "download_x_item": "Scarica {{item_count}} elementi", - "download_button": "Scarica", - "using_optimized_server": "Utilizzando il server di ottimizzazione", - "using_default_method": "Utilizzando il metodo predefinito" - } - }, - "live_tv": { - "next": "Prossimo", - "previous": "Precedente", - "live_tv": "TV in diretta", - "coming_soon": "Prossimamente", - "on_now": "In onda ora", - "shows": "Programmi", - "movies": "Film", - "sports": "Sport", - "for_kids": "Per Bambini", - "news": "Notiziari" - }, - "jellyseerr":{ - "confirm": "Conferma", - "cancel": "Cancella", - "yes": "Si", - "whats_wrong": "Cosa c'è che non va?", - "issue_type": "Tipo di problema", - "select_an_issue": "Seleziona un problema", - "types": "Tipi", - "describe_the_issue": "(facoltativo) Descrivere il problema...", - "submit_button": "Invia", - "report_issue_button": "Segnalare il problema", - "request_button": "Richiedi", - "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", - "failed_to_login": "Accesso non riuscito", - "cast": "Cast", - "details": "Dettagli", - "status": "Stato", - "original_title": "Titolo originale", - "series_type": "Tipo di Serie", - "release_dates": "Date di Uscita", - "first_air_date": "Prima Data di Messa in Onda", - "next_air_date": "Prossima Data di Messa in Onda", - "revenue": "Ricavi", - "budget": "Budget", - "original_language": "Lingua Originale", - "production_country": "Paese di Produzione", - "studios": "Studio", - "network": "Network", - "currently_streaming_on": "Attualmente in streaming su", - "advanced": "Avanzate", - "request_as": "Richiedi Come", - "tags": "Tag", - "quality_profile": "Profilo qualità", - "root_folder": "Cartella radice", - "season_x": "Stagione {{seasons}}", - "season_number": "Stagione {{season_number}}", - "number_episodes": "{{episode_number}} Episodio", - "born": "Nato", - "appearances": "Aspetto", "toasts": { - "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", - "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", - "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", - "issue_submitted": "Problema inviato!", - "requested_item": "Richiesto {{item}}!", - "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", - "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" + "error_deleting_files": "Errore nella cancellazione dei file", + "background_downloads_enabled": "Scaricamento in background abilitato", + "background_downloads_disabled": "Scaricamento in background disabilitato", + "connected": "Connesso", + "could_not_connect": "Non è stato possibile connettersi", + "invalid_url": "URL invalido" } }, - "tabs": { - "home": "Home", - "search": "Cerca", - "library": "Libreria", - "custom_links": "Collegamenti personalizzati", - "favorites": "Preferiti" + "downloads": { + "downloads_title": "Scaricati", + "tvseries": "Serie TV", + "movies": "Film", + "queue": "Coda", + "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", + "no_items_in_queue": "Nessun elemento in coda", + "no_downloaded_items": "Nessun elemento scaricato", + "delete_all_movies_button": "Cancella tutti i film", + "delete_all_tvseries_button": "Cancella tutte le serie TV", + "delete_all_button": "Cancella tutti", + "active_download": "Scaricamento in corso", + "no_active_downloads": "Nessun scaricamento in corso", + "active_downloads": "Scaricamenti in corso", + "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", + "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", + "back": "Indietro", + "delete": "Cancella", + "something_went_wrong": "Qualcosa è andato storto", + "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodi", + "toasts": { + "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", + "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", + "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", + "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", + "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", + "download_cancelled": "Scaricamento annullato", + "could_not_cancel_download": "Impossibile annullare lo scaricamento", + "download_completed": "Scaricamento completato", + "download_started_for": "Scaricamento iniziato per {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", + "download_stated_for_item": "Scaricamento iniziato per {{item}}", + "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", + "download_completed_for_item": "Scaricamento completato per {{item}}", + "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", + "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", + "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", + "go_to_downloads": "Vai agli elementi scaricati" + } } - } \ No newline at end of file + }, + "search": { + "search_here": "Cerca qui...", + "search": "Cerca...", + "x_items": "{{count}} elementi", + "library": "Libreria", + "discover": "Scopri", + "no_results": "Nessun risultato", + "no_results_found_for": "Nessun risultato trovato per", + "movies": "Film", + "series": "Serie", + "episodes": "Episodi", + "collections": "Collezioni", + "actors": "Attori", + "request_movies": "Film Richiesti", + "request_series": "Serie Richieste", + "recently_added": "Aggiunti di Recente", + "recent_requests": "Richiesti di Recente", + "plex_watchlist": "Plex Watchlist", + "trending": "In tendenza", + "popular_movies": "Film Popolari", + "movie_genres": "Generi Film", + "upcoming_movies": "Film in arrivo", + "studios": "Studio", + "popular_tv": "Serie Popolari", + "tv_genres": "Generi Televisivi", + "upcoming_tv": "Serie in Arrivo", + "networks": "Network", + "tmdb_movie_keyword": "TMDB Parola chiave del film", + "tmdb_movie_genre": "TMDB Genere Film", + "tmdb_tv_keyword": "TMDB Parola chiave della serie", + "tmdb_tv_genre": "TMDB Genere Televisivo", + "tmdb_search": "TMDB Cerca", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", + "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" + }, + "library": { + "no_items_found": "Nessun elemento trovato", + "no_results": "Nessun risultato", + "no_libraries_found": "Nessuna libreria trovata", + "item_types": { + "movies": "film", + "series": "serie TV", + "boxsets": "cofanetti", + "items": "elementi" + }, + "options": { + "display": "Display", + "row": "Fila", + "list": "Lista", + "image_style": "Stile dell'immagine", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Mostra titoli", + "show_stats": "Mostra statistiche" + }, + "filters": { + "genres": "Generi", + "years": "Anni", + "sort_by": "Ordina per", + "sort_order": "Criterio di ordinamento", + "tags": "Tag" + } + }, + "favorites": { + "series": "Serie TV", + "movies": "Film", + "episodes": "Episodi", + "videos": "Video", + "boxsets": "Boxset", + "playlists": "Playlist" + }, + "custom_links": { + "no_links": "Nessun link" + }, + "player": { + "error": "Errore", + "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", + "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", + "client_error": "Errore del client", + "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", + "message_from_server": "Messaggio dal server", + "video_has_finished_playing": "La riproduzione del video è terminata!", + "no_video_source": "Nessuna sorgente video...", + "next_episode": "Prossimo Episodio", + "refresh_tracks": "Aggiorna tracce", + "subtitle_tracks": "Tracce di sottotitoli:", + "audio_tracks": "Tracce audio:", + "playback_state": "Stato della riproduzione:", + "no_data_available": "Nessun dato disponibile", + "index": "Indice:" + }, + "item_card": { + "next_up": "Il prossimo", + "no_items_to_display": "Nessun elemento da visualizzare", + "cast_and_crew": "Cast e Equipaggio", + "series": "Serie", + "seasons": "Stagioni", + "season": "Stagione", + "no_episodes_for_this_season": "Nessun episodio per questa stagione", + "overview": "Panoramica", + "more_with": "Altri con {{name}}", + "similar_items": "Elementi simili", + "no_similar_items_found": "Non sono stati trovati elementi simili", + "video": "Video", + "more_details": "Più dettagli", + "quality": "Qualità", + "audio": "Audio", + "subtitles": "Sottotitoli", + "show_more": "Mostra di più", + "show_less": "Mostra di meno", + "appeared_in": "Apparso in", + "could_not_load_item": "Impossibile caricare l'elemento", + "none": "Nessuno", + "download": { + "download_season": "Scarica Stagione", + "download_series": "Scarica Serie", + "download_episode": "Scarica Episodio", + "download_movie": "Scarica Film", + "download_x_item": "Scarica {{item_count}} elementi", + "download_button": "Scarica", + "using_optimized_server": "Utilizzando il server di ottimizzazione", + "using_default_method": "Utilizzando il metodo predefinito" + } + }, + "live_tv": { + "next": "Prossimo", + "previous": "Precedente", + "live_tv": "TV in diretta", + "coming_soon": "Prossimamente", + "on_now": "In onda ora", + "shows": "Programmi", + "movies": "Film", + "sports": "Sport", + "for_kids": "Per Bambini", + "news": "Notiziari" + }, + "jellyseerr": { + "confirm": "Conferma", + "cancel": "Cancella", + "yes": "Si", + "whats_wrong": "Cosa c'è che non va?", + "issue_type": "Tipo di problema", + "select_an_issue": "Seleziona un problema", + "types": "Tipi", + "describe_the_issue": "(facoltativo) Descrivere il problema...", + "submit_button": "Invia", + "report_issue_button": "Segnalare il problema", + "request_button": "Richiedi", + "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", + "failed_to_login": "Accesso non riuscito", + "cast": "Cast", + "details": "Dettagli", + "status": "Stato", + "original_title": "Titolo originale", + "series_type": "Tipo di Serie", + "release_dates": "Date di Uscita", + "first_air_date": "Prima Data di Messa in Onda", + "next_air_date": "Prossima Data di Messa in Onda", + "revenue": "Ricavi", + "budget": "Budget", + "original_language": "Lingua Originale", + "production_country": "Paese di Produzione", + "studios": "Studio", + "network": "Network", + "currently_streaming_on": "Attualmente in streaming su", + "advanced": "Avanzate", + "request_as": "Richiedi Come", + "tags": "Tag", + "quality_profile": "Profilo qualità", + "root_folder": "Cartella radice", + "season_x": "Stagione {{seasons}}", + "season_number": "Stagione {{season_number}}", + "number_episodes": "{{episode_number}} Episodio", + "born": "Nato", + "appearances": "Aspetto", + "toasts": { + "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", + "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", + "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", + "issue_submitted": "Problema inviato!", + "requested_item": "Richiesto {{item}}!", + "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", + "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" + } + }, + "tabs": { + "home": "Home", + "search": "Cerca", + "library": "Libreria", + "custom_links": "Collegamenti personalizzati", + "favorites": "Preferiti" + } +} diff --git a/translations/ja.json b/translations/ja.json index 743f1e22..2f43f5ae 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -342,7 +342,7 @@ "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", "client_error": "クライアントエラー", "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", - "message_from_server": "サーバーからのメッセージ: {{message}}", + "message_from_server": "サーバーからのメッセージ", "video_has_finished_playing": "ビデオの再生が終了しました!", "no_video_source": "動画ソースがありません...", "next_episode": "次のエピソード", diff --git a/translations/nl.json b/translations/nl.json index 929224c9..7ab85468 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -147,7 +147,7 @@ "default": "Standaard", "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", - "url":"URL", + "url": "URL", "server_url_placeholder": "http(s)://domein.org:poort" }, "plugins": { @@ -204,7 +204,7 @@ "app_language_description": "Selecteer een taal voor de app.", "system": "Systeem" }, - "toasts":{ + "toasts": { "error_deleting_files": "Fout bij het verwijden van bestanden", "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", @@ -343,7 +343,7 @@ "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", "client_error": "Fout van de client", "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", - "message_from_server": "Bericht van de server: {{message}}", + "message_from_server": "Bericht van de server", "video_has_finished_playing": "Video is gedaan met spelen!", "no_video_source": "Geen video bron...", "next_episode": "Volgende Aflevering", @@ -399,7 +399,7 @@ "for_kids": "Voor kinderen", "news": "Nieuws" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Bevestig", "cancel": "Annuleer", "yes": "Ja", diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 4e04ad6f..b501cef0 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -342,7 +342,7 @@ "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", "client_error": "客户端错误", "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", - "message_from_server": "来自服务器的消息:{{message}}", + "message_from_server": "来自服务器的消息", "video_has_finished_playing": "视频播放完成!", "no_video_source": "无视频来源...", "next_episode": "下一集", diff --git a/translations/zh-TW.json b/translations/zh-TW.json index bc4e9136..21800640 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -342,7 +342,7 @@ "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", "client_error": "客戶端錯誤", "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", - "message_from_server": "來自伺服器的消息:{{message}}", + "message_from_server": "來自伺服器的消息", "video_has_finished_playing": "影片播放完畢!", "no_video_source": "無影片來源...", "next_episode": "下一集", diff --git a/utils/profiles/native.js b/utils/profiles/native.js index 72d4e3b6..92f36b02 100644 --- a/utils/profiles/native.js +++ b/utils/profiles/native.js @@ -28,7 +28,7 @@ export default { Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls", VideoCodec: "h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video", - AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma", + AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts", }, { Type: MediaTypes.Audio, From cf284eb3d8ab5d59cab40d6d6503d23ab1dcbdd3 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Sun, 23 Feb 2025 18:42:00 +0100 Subject: [PATCH 23/93] fix: sort sessions by name --- hooks/useSessions.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index ec8d3189..2a2f9b09 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -9,14 +9,11 @@ export interface useSessionsProps { activeWithinSeconds: number; } -export const useSessions = ({ - refetchInterval = 5 * 1000, - activeWithinSeconds = 360, -}: useSessionsProps) => { +export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const { data, isLoading, error } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ["sessions"], queryFn: async () => { if (!api || !user || !user.Policy?.IsAdministrator) { @@ -25,11 +22,11 @@ export const useSessions = ({ const response = await getSessionApi(api).getSessions({ activeWithinSeconds: activeWithinSeconds, }); - return response.data.filter((s) => s.NowPlayingItem); + return response.data + .filter((s) => s.NowPlayingItem) + .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); }, refetchInterval: refetchInterval, - //enabled: !!user || !!user.Policy?.IsAdministrator, - //cacheTime: 0 }); return { sessions: data, isLoading }; From f64c5a02dbe1c1c32ed2bc4d016c41b340f402ce Mon Sep 17 00:00:00 2001 From: sarendsen Date: Sun, 23 Feb 2025 19:15:10 +0100 Subject: [PATCH 24/93] fix: add hw/sw badge to session --- app/(auth)/(tabs)/(home)/sessions/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index 8da32bce..ccfdb459 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -4,7 +4,7 @@ import { FlashList } from "@shopify/flash-list"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { Loader } from "@/components/Loader"; -import { SessionInfoDto } from "@jellyfin/sdk/lib/generated-client"; +import { HardwareAccelerationType, SessionInfoDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtomValue } from "jotai"; import { apiAtom } from "@/providers/JellyfinProvider"; import Poster from "@/components/posters/Poster"; @@ -186,6 +186,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { resolution: , language: , audioChannels: , + hwType: , } as const; const icon = (val: string) => { @@ -200,6 +201,8 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { switch (key) { case "bitrate": return formatBitrate(val); + case "hwType": + return val === HardwareAccelerationType.None ? "sw" : "hw"; default: return val; } @@ -219,6 +222,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { }; interface StreamProps { + hwType?: HardwareAccelerationType | null | undefined; resolution?: string | null | undefined; language?: string | null | undefined; codec?: string | null | undefined; @@ -313,6 +317,7 @@ const TranscodingView = ({ session }: SessionCardProps) => { codec: videoStream?.Codec, }} transcodeProperties={{ + hwType: session.TranscodingInfo?.HardwareAccelerationType, bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.VideoCodec, }} @@ -332,7 +337,6 @@ const TranscodingView = ({ session }: SessionCardProps) => { audioChannels: audioStream?.ChannelLayout, }} transcodeProperties={{ - bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.AudioCodec, audioChannels: session.TranscodingInfo?.AudioChannels?.toString(), }} From 89bf51c3cc061fdd8cc52a0face341663ee33464 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 24 Feb 2025 09:30:14 +0100 Subject: [PATCH 25/93] fix: playback reporting --- app/(auth)/player/direct-player.tsx | 68 ++++++++++++----------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 51d5c3dc..46ff6f68 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -29,7 +29,12 @@ import { useSharedValue } from "react-native-reanimated"; import { useSettings } from "@/utils/atoms/settings"; import { useTranslation } from "react-i18next"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; +import { + BaseItemDto, + MediaSourceInfo, + PlaybackProgressInfo, + PlaybackStartInfo, +} from "@jellyfin/sdk/lib/generated-client"; export default function page() { const videoRef = useRef(null); @@ -212,6 +217,19 @@ export default function page() { }; }, [navigation, stop]); + const currentPlayStateInfo = () => { + return { + itemId: item?.Id!, + audioStreamIndex: audioIndex ? audioIndex : undefined, + subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, + mediaSourceId: mediaSourceId, + positionTicks: msToTicks(progress.get()), + isPaused: !isPlaying, + playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: stream.sessionId, + }; + }; + const onProgress = useCallback( async (data: ProgressUpdatePayload) => { if (isSeeking.get() || isPlaybackStopped) return; @@ -225,20 +243,9 @@ export default function page() { if (offline) return; - const currentTimeInTicks = msToTicks(currentTime); - if (!item?.Id || !stream) return; - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item.Id, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(currentTimeInTicks), - isPaused: !isPlaying, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream.sessionId, - }); + changePlaybackState(); }, [item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering] ); @@ -248,22 +255,12 @@ export default function page() { setIsPipStarted(pipStarted); }, []); - const changePlaybackState = useCallback( - async (isPlaying: boolean) => { - if (!api || offline || !stream) return; - await getPlaystateApi(api).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: msToTicks(progress.get()), - isPaused: !isPlaying, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream.sessionId, - }); - }, - [api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress] - ); + const changePlaybackState = useCallback(async () => { + if (!api || offline || !stream) return; + await getPlaystateApi(api).reportPlaybackProgress({ + playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo, + }); + }, [api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]); const startPosition = useMemo(() => { if (offline) return 0; @@ -272,14 +269,7 @@ export default function page() { const reportPlaybackStart = useCallback(async () => { if (offline || !stream) return; - await getPlaystateApi(api!).onPlaybackStart({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId ? stream?.sessionId : undefined, - }); + await getPlaystateApi(api!).reportPlaybackStart({ playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo }); hasReportedRef.current = true; }, [api, item, stream]); @@ -304,14 +294,14 @@ export default function page() { if (state === "Playing") { setIsPlaying(true); - await changePlaybackState(true); + await changePlaybackState(); if (!Platform.isTV) await activateKeepAwakeAsync(); return; } if (state === "Paused") { setIsPlaying(false); - await changePlaybackState(false); + await changePlaybackState(); if (!Platform.isTV) await deactivateKeepAwake(); return; } From 35c9258062355a4ebae41f5e666656bc31ce7013 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 24 Feb 2025 10:30:01 +0100 Subject: [PATCH 26/93] fix: playback pause/play reporting --- app/(auth)/player/direct-player.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 46ff6f68..30f85f25 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -180,16 +180,15 @@ export default function page() { fetchStreamData(); }, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]); - const togglePlay = useCallback(async () => { - if (!api) return; - + const togglePlay = async () => { lightHapticFeedback(); + setIsPlaying(!isPlaying); if (isPlaying) { await videoRef.current?.pause(); } else { videoRef.current?.play(); } - }, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]); + } const reportPlaybackStopped = useCallback(async () => { if (offline) return; @@ -245,7 +244,7 @@ export default function page() { if (!item?.Id || !stream) return; - changePlaybackState(); + reportPlaybackProgress(); }, [item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering] ); @@ -255,12 +254,12 @@ export default function page() { setIsPipStarted(pipStarted); }, []); - const changePlaybackState = useCallback(async () => { + const reportPlaybackProgress = useCallback(async () => { if (!api || offline || !stream) return; await getPlaystateApi(api).reportPlaybackProgress({ playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo, }); - }, [api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]); + }, [api, isPlaying, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]); const startPosition = useMemo(() => { if (offline) return 0; @@ -291,17 +290,16 @@ export default function page() { const onPlaybackStateChanged = useCallback( async (e: PlaybackStatePayload) => { const { state, isBuffering, isPlaying } = e.nativeEvent; - if (state === "Playing") { setIsPlaying(true); - await changePlaybackState(); + reportPlaybackProgress(); if (!Platform.isTV) await activateKeepAwakeAsync(); return; } if (state === "Paused") { setIsPlaying(false); - await changePlaybackState(); + reportPlaybackProgress(); if (!Platform.isTV) await deactivateKeepAwake(); return; } @@ -313,7 +311,7 @@ export default function page() { setIsBuffering(true); } }, - [changePlaybackState] + [reportPlaybackProgress] ); const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || []; From 00bc4232fb641af0ab52bbf64efd2879691b34b5 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 24 Feb 2025 11:51:48 +0100 Subject: [PATCH 27/93] fix: xcode warnings --- modules/vlc-player/ios/VlcPlayerView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index e2614fa6..c40b5108 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -402,7 +402,7 @@ class VlcPlayerView: ExpoView { } private func updateVideoProgress() { - guard let media = self.vlc.player.media else { return } + guard self.vlc.player.media != nil else { return } let currentTimeMs = self.vlc.player.time.intValue let durationMs = self.vlc.player.media?.length.intValue ?? 0 @@ -459,7 +459,7 @@ extension VlcPlayerView: SimpleAppLifecycleListener { } // Current solution to fixing black screen when re-entering application - if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, + if let videoTrack = self.vlc.player.videoTracks.first(where: { $0.isSelected == true }), !self.vlc.isMediaPlaying() { videoTrack.isSelected = false @@ -479,6 +479,7 @@ extension VLCMediaPlayerState { case .paused: return "Paused" case .stopped: return "Stopped" case .error: return "Error" + case .stopping: return "Stopping" @unknown default: return "Unknown" } } From 5273dfd22be0f9671d38cc6f6d385f39b4ec986b Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 24 Feb 2025 14:21:52 +0100 Subject: [PATCH 28/93] small cleanup --- app/(auth)/player/direct-player.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 30f85f25..82b5c2e3 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -255,7 +255,7 @@ export default function page() { }, []); const reportPlaybackProgress = useCallback(async () => { - if (!api || offline || !stream) return; + if (!api || offline || !stream || !hasReportedRef.current) return; await getPlaystateApi(api).reportPlaybackProgress({ playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo, }); @@ -276,7 +276,6 @@ export default function page() { useEffect(() => { if (stream && !hasReportedRef.current) { reportPlaybackStart(); - hasReportedRef.current = true; // Mark as reported } }, [stream]); From ef425103830dc4352b1265acaf1433488b661cb8 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 24 Feb 2025 14:56:39 +0100 Subject: [PATCH 29/93] small cleanup --- app/(auth)/(tabs)/(home)/sessions/index.tsx | 4 ++-- app/(auth)/player/direct-player.tsx | 15 +-------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index ccfdb459..5c2c1992 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -300,8 +300,8 @@ const TranscodingView = ({ session }: SessionCardProps) => { }, [session.PlayState?.SubtitleStreamIndex]); const isTranscoding = useMemo(() => { - return session.PlayState?.PlayMethod == "Transcode"; - }, [session.PlayState?.PlayMethod]); + return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo; + }, [session.PlayState?.PlayMethod, session.TranscodingInfo]); const videoStreamTitle = () => { return videoStream?.DisplayTitle?.split(" ")[0]; diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 82b5c2e3..f0eeb426 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -255,7 +255,7 @@ export default function page() { }, []); const reportPlaybackProgress = useCallback(async () => { - if (!api || offline || !stream || !hasReportedRef.current) return; + if (!api || offline || !stream) return; await getPlaystateApi(api).reportPlaybackProgress({ playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo, }); @@ -266,19 +266,6 @@ export default function page() { return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0; }, [item]); - const reportPlaybackStart = useCallback(async () => { - if (offline || !stream) return; - await getPlaystateApi(api!).reportPlaybackStart({ playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo }); - hasReportedRef.current = true; - }, [api, item, stream]); - - const hasReportedRef = useRef(false); - useEffect(() => { - if (stream && !hasReportedRef.current) { - reportPlaybackStart(); - } - }, [stream]); - useWebSocket({ isPlaying: isPlaying, togglePlay: togglePlay, From 9c6aebe66acade794755646b6dd5b5a223025f84 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 24 Feb 2025 18:41:07 +0100 Subject: [PATCH 30/93] small cleanup --- app/(auth)/player/direct-player.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index f0eeb426..9a407ade 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -32,8 +32,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BaseItemDto, MediaSourceInfo, + PlaybackOrder, PlaybackProgressInfo, PlaybackStartInfo, + RepeatMode, } from "@jellyfin/sdk/lib/generated-client"; export default function page() { @@ -188,7 +190,7 @@ export default function page() { } else { videoRef.current?.play(); } - } + }; const reportPlaybackStopped = useCallback(async () => { if (offline) return; @@ -226,6 +228,10 @@ export default function page() { isPaused: !isPlaying, playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", playSessionId: stream.sessionId, + isMuted: false, + canSeek: true, + repeatMode: RepeatMode.RepeatNone, + playbackOrder: PlaybackOrder.Default, }; }; From caa4b765c1713afea3bc7a902ef4a671926e6c40 Mon Sep 17 00:00:00 2001 From: Simon Eklundh Date: Wed, 26 Feb 2025 08:23:27 +0100 Subject: [PATCH 31/93] fix: makes the icon adaptive for android (#569) --- app.json | 4 +++- assets/images/adaptive_icon.png | Bin 93379 -> 80481 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app.json b/app.json index 140a7f9a..8867d0cc 100644 --- a/app.json +++ b/app.json @@ -33,7 +33,9 @@ "jsEngine": "hermes", "versionCode": 53, "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive_icon.png" + "foregroundImage": "./assets/images/adaptive_icon.png", + "backgroundColor": "#464646" + }, "package": "com.fredrikburmester.streamyfin", "permissions": [ diff --git a/assets/images/adaptive_icon.png b/assets/images/adaptive_icon.png index 8443e717456436c0221fff6da729a34bcde026bd..3d08940d8bde48a87e3de62cdbb89ac0c304b337 100644 GIT binary patch literal 80481 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 bI85$9;G8jB3st_5yY!pa46hbkv3d4?K@c3# literal 93379 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ℜRfE zPJNL-9xD@a_hyDS(QO58#P1{{S zqX%g(c)Et%#xDE&y`p{5;&M z_66+J`=CZA=EeLBJ)QR3ejdK+Hq=#pM9WNg74xm@V~R=-87!W%zJ1ZW-m498vG7%< zGq{Ryb$q2r@3B8gRnweRnZi&5VGKBpvI?2e(AgE{oK{{O35gr^x>~p*urstP$V!48 zsHRp|g}k+}J&mA%eZpi;(2?k|QNbK8rwtJ9qn;O29v~Ish8;W34e%%sMyEB^zdyhm z zlH2AS#CkhX`)hgd7DZC*ElZp5XS@Z2&0djpecZdCvq&btY3Cu9n2w@*v_)%<;e`b^yU0CRZjh>?`c zxc>v4BTk6ML6ofAz_zY`j;9Wrkk!(NZ%5?^Idb<>n68bUO_?wl26q^I{ZQR&UUO$H zv1@O#;J(9^p8mhrcD8|*4?iB%fAvok`9*)^vK}m3Z8vl+AcsjKtblNaHAm?S6w?8%HFqZ5EV6 z!9iBPm-o~se;=HJ*Afx#th())fx2!2OV~t3=qlzl9ip!v zYf1Wxy2$czCUyRKP&M#6=J9?sW=vLivDDGH^GzIYpe^HafR|xT_f5Nm?|3t<8IWjr zEL>!g-mi;%G}>{L=Yy?&Pb1Nc>46ZNP4!9ru)`BgrA$+i&DAgD>q`cQXql6~)&T@? zcBQY9L@TDK9=>asP%5NfGEX|@&78f~l(VyOgdlo;>eYLm-ED6PA>m1qvil54h1)sr zmSooI+!tHrHMq8>-I7tST42UujR=v*M21u|@a876!S*J*(-*mWyMuFV{_)Y#7=Z}1 z=o_Z-@pF9f_Pg2v^od^$P5D*X!fQzag{i(~K{SuuIK+ z@Ao%#)b^;n;UI9=Nj%A?kV}7OSqm0c1)XotT>gQkg~Kt~WJF8rY%-121MA`YSY@Bn zxncCd_~nnw({wZXnkR9t`z}H~vJyhIG`HNb7BQUvXXVd}Wf@8)4fW&x_itY4R)B(t zT~pAy##qlK5J2P0v%f|UaBFP5?aMswN^`2=SjjE27%^#XP}$7V2d|+>p;6(B?>z?w z5jJTB2>|g|%f4QXN-Hw--EM%KR%g|Hspd7Cq~GIuGEkL8TDyln+>ri5sfnc)$rvwt zu-fSPzMXN4WVmsUCE74t%1M)3@FCFTXg|3)<0w^~(B8Sl-}4J(Q?YoYhQ!YUabZ?> zEGEP4)GEY%Hr1^1qe{Q0Ezx8mR%h)x8=jVTUWZ9gFj=3Bb~SDPd?40md-wxjb*|>Mwgh=7(aI#S&)zf; ze%L&F()r9F4Y75%qBM2L2<7N@m@$~!+4Q=m`&MU-={)=M*3bwIo;J-qv~-n-ZAPqN z+slSqUj2PA)pzBK;V7iVG_P=Uf2Xd2$#okpbGoj>QwAQ$w=GtQMZdq8ZNPi=UUG?v_!_FZbD_Ca`Xo zaiKQnQh+X`QhbedN&Mm2Bs=zuPMVQH3QKg`zz1KB%?GlU)Zn(Xc~-~ZIAwcRxVbMA z4mmr>wHSDF6{vw5$i?USkB@)Q`1~BB*3ndUp)7FK?CpgKJ^8xoJF-lF6cb(S3kmQ< zOPw((lI_&cNKUiO{_0In`LzuaW;$y8mAoq!;IT5V8d4V|+T6^f8X^^`ecncytDVlw zwAsY37~F6<>H5H5y%9MM+WWwQfSrnoi%A}=x6aO2M4RL_#5!(=pY+mCFJ`YXo21~x z&DR&^mz6a*8;$Av(CC&%I6Uey+gjR6+M&eWejxO;>2xVKrLItU#OI zFn$go$vcxrqouJnb2_{{{;2N_JkiVe=AbzzQ+8GZubhGHI$5{60?QeW;KYeCLE_nS zt)yfxK@h#7@ItRfC;i2wD8=v~Db$~K4(w3+h+%JyP`DJbga;~gpIiEf9ZN5AdAxGm z7u<JN<(5Rnz-? zIx@t19b6J)p?%A#Rkd%GeUu{a(N8~+ioJ{1Q+1nA5TTAzg6;c72yaEWvb^&xL4?xb+kS6x+^}EXBcGXRX|t&nBO8giKo!ZJ2^&K_7S!F z8V+_63+klfZBGG5c?>(7(|})m$WW}nj|nPZKT7H7>sF@CLG~keIRcN*>pRc)W#~H` zZ7+9vEDK-+qz;_ej51z}8LmCxKgY|A-s3L7XN@XbY-2e!nQDR%ykehR!@>@7`Tp0OnR&r@e!1))^&%WY8>|PtMa! zqD>aJm&7xa@qaDM!9OvtPl#ySbPnab=D!nKDAU-Hu02%YYa70{mG40Ba)cT636`r- z{P9VjJT0Q2sMql0lo~Vbl0TEI6&N}b)4 z>H}EpZhq+(7HI@*VO@!je!~$4`IvWcrJ49q&SdJ7MVAB~Lq_t1W)>O1_^%xWdYk3>O-p~7!EQfCyg4gmsH@FcD1iYI&xH*0uc8kk& z3P)9o{>-ttyTC<^3sEinx4k?5u`{h2TONXG(axhY`NV6RpDzxFPc03y+BUx*@cD~0 zuY-_J5@U%M2JZ0n*xMJVG?m-sO*Y1hy8d{uXVo}dPO64Ee)D3UO5mq$Xrz65T&$Y^ zSi%2e=_>=8e!sT~ML+}u1irK&Eg;>Egp{;2igb5(H%RB`?(UEV>1K@Xn#33k*#7hP zJa6{)v-`eJUFTfqob1+~jha(-K$n~D0r5VABg{PwyDnGvmj^q}egTp+171t>L2hv| z<6Y6wyf_%!F=v2fx{8&0MBVp2=3pM zk0CFJg|u8A-2H0CkhHCTXW8#c#cEWbO4qwgCI?U6^^@Y)DXbn<{KV zkyP^D%Pkjic>S|F!>+(q4@o+3Z_W7k!KzqzcFYDR{)unO0_G6^WN?dq_?fgG3vx#1s%d<&Y& z+#6od?Kky20U2sWBNj23Ivb;Jm<(rmmgXakp*2sUDssMqWiEVAz15S~@MP*F#)KYK zG2c_4{iCgWz?MA5bJokR#8ggn(Qn=fpK4(sj68~}G1MiI~#5Pp8KQwD}{iEHCMjKbagmbb8I<@N4to_0;ARh^I`uxGr*hvN=aSm9nSJz$G-b&^AIt9oh; zbiB@7rmnl4I!Kx_K5MEdT9NqTh>o7K;N9eO zb#v-bZ+C(3{gKc`@!IFeUlD$gjgXi|vQN%;k^+l7DFTslw_*7&qTg8n(XRt1e48jX zB-|}GD1+j z18*e2A;&Q7h-%|k#O21XmV;SVtP73-Phq1*!w78`C(U1ChD=x6#S7`Eq6$Bz4ALun z6|8bHh|-r7cB24yqjZ{Ak@x{GygRkuz4-L5Q4(GM=)TJoolaq25yv~4WO*Q^ind@B z(CyrpfnqzSqJcHuI#G=;!S1JS3kTtu>lYd7Y&|Up-RnLPP5i>$-&}gN{|J7VMDi8z z#>F3|x6(Os5#0uB9UG7G{l#xijRIC3u>N5MezJd^VbVusRL@r2LTvVnLZFhY=e=wr z$jL%$hl4Z!vK}U>a$8n%V~fb4-M-)H-6*nfZNXNue{RJ&QsJg+$aRw?HGbxA)f8F# zruoEUV{?730RPPEg+$rYRI<5VFY!%rX4PuFP21teyR^@EI}UxZ<}bRP+>s2gH^_D2 zd@HgEFMkFkgG&*>cnW^$B0%=fOP3rYSi^XWK>l5m{MLFrYV^EprRtblrN4tOUmJTx z7EcWVv!RK4@SoHhvQC!Cx$UOl1`sJW3ASwbSX%tLPnA;%9i1 z>%g3WU;I%7L=jT~ut_OQLUO1a<}DJo1h+a!K=0aYkz1bj^arOOFSr%2r-y??oLBDd z<4`@Qfmdcgn{T^r#{uY4-dY?!GYCm12#>dJ3>m+SAqKO>(=UpRFwK&qF$iT(I`%H?@6o2NTrtHWVT~!C+>!}N_z=#2SccDt5B{9 zwHLnZ>}|L06Fv|ax5Q3kG2GoH7NW7wj8ZPhl&|h;4_RY*Yc)|HEN;ku_#c_@;MU5P zzz-Z{|EA8f)m5zVRMNm|4tWyCjJ4I|&~?YbXJX#LjN@w$h8MCps7;F>r2Ry#fAKy4QQ3E}-;q!h0hL_)Z_5BZW0qHTo zqio4Qr)AOV*Y(E#ynS3C(#=tbfk4@O(CjIuYx2?guc})cGf_d!8W&SV?;xB24fJqd zD7zT;?46?BNPgUyOm%XCLHVs5&aeD>bJ*qYy|~Vl6I_5ktQA5`ywZjZ91KEuaNvdq zR3RJDJ@zwS)lEJ#di-?Zt@N*g=kio{^~eNiPu`@|x1aBlxb=yv^gJr%7M}Y{&MW18 z!R(rxQqbvRtbIuv1~{xIUssX7IshBMzkfX{yV6%8=P3&N+%>#4kKCixkVCB1zSD5A z5~X_<@gSPJj@xRqzUQ-7R@ZoDklj2Nat2NUc>Z;mc%|a692NpAF` z*_Qo?D~}8mCPRi`;OQwzB;cj$#Z$`oqwFf$XUyl^=_DVMS? z@n;|p9ghGL%Q>dF;BRlI`zcji@zuA{fgiYJ&{FVGbYWRsE|+uw?8^!gn= zbZv>`7Og)^+flGVtM(1jAJOEOPGU?Gk7x=W%Q11zhJd(!bia7chlR0jE}b(p z--i}a*u+`a?PU+A^5=*wLaX3aha<+eMsqe3qKtOX!k4K--~@nBO0fUxrbIyk4BE( zhNL67p3|wH17?kTk<)S=T=SgwOEqshT3Mt=kg*2mC!{#>1=`^^S-Un`mp8thres}f zx;T6tr*mK|!?t?x#Mjupp|~F*mk~kN2?a71^;Hqh>afN=1^qU zj!lmyk=1y5zs3kSw6pu2oB$s5tsdtvwixYKC@(9*S|D$8KUOu`EaFHH4}7I6Fzyrg z2S2~i=X$Vid?nbpcyM^&G@@|NE}+hv{x?gzAf8hL$-ES-?T<_*w#goUC_NWg1lNrA zhU&EgfPtK4YxkPFZrSh~lu;`3HkiSmCs>A#rr9afHPaQj4}Gd(Yx8}#YEy;>_1Tc- zx;;^9mh<s0J!aq$81U$pe63K)?$Z6##6m66DGSdsEHa};|t|P*v za0+Xqx7ZW4D407H&cmm({9ZpL3IDzL#zx$X?-ZV@%-WSWy!QnrGl1?JJ{aBa?_Y!m zoJp201oyN_2mGC<>T&lm02|D~S@aP(3Hrfx+aYoIW-Zks*}hhYoi=g>@gH;>(ih=S ztcUv6vW}=sI;V3D)7)qui)Jt-B=3ig6l61bOn&aq(#mgx?#TEy6u0kFE`?MSxXi!w zC1?5`SC`{NlaHUjoRF)f3wm4VnMP0Na%GjD3+WiUP^d%ROP6STBIFd5wlO`!_qGNL z!hB{BfHN!96?cI3O}g0LWGqG?k}T#((2wi{4jf3@ud~m`Ix)B0xLsk@i|DD-8e9dx zW3z6tUZ`GbcxZ;u`o5RRspCU@JjJfEn{eGT4x6)id+*#pA~2-+uF5vsPD0 zV~yutu(`^G;I!H0dcwBk+&199_CX-QJ5KR2vM`10Z@g^V35s<6G3nT_7;5ptGZO4| zpp^_6E7r09OYf_$#qL9yVq)lBy8SNgbC)Yg=At@a++luM5=OW19V9FnkgQOp%;-$Ro{D#fm7!NQFR@;b^{?r{-W-&)x zYU#L`%|0xGeWGzNj^s#A97LZzPLfFwY%s<;M8I+G^NJPA!cTTM*BP3LGSW}Ah9Ivf zaA4<81Nb6_EOTAeGCcf7fW5SPtZhE};)g0l?Asl}tQI%S=~0$1-icUmkCa_)qXIxS zC4=-0jVifCA`M{3e2XK&db~)#g>d5SU}gdz@zitTL;d5|+Y@K-l^}biUO6%L)05&9 z3h1>k7^xiw8Vs>)E%uMYou@gESGNfV=|B=089~yYy zf?0Uoalc-yw5io`<}fF!SAhGh+vP!9_2jNB^o_VQS*A_q36S_m@?`c;Zk#8 z|BA!KENJwl-z_Xs?BVIH;p$Y(#Mnm3Pg>R}l3MJFopXXssU1JM{gg)HeIa^|-44H_ zcIv+4qp4n-=FEOu^i%(Hsh?ZQ%k4~XkW{e;wj-D$UB?~Fid~1VZT>3V(I8@+?il+p zBr+f&jI3$i@LONZkE^;(*&lLF_bphVv0YUph5vOdHPqEhVfch5|XT|}Uk_cbRrayH?I|5;x&IW~XBYGqTUmI8>BF2!q0Y~|V6 zd$%_~70B=4JrD~ie>-vWo;|2=H9#uQ06cQDo4Zu}rhpDM24OZ7V4RR39;K;t z{s7D6W6UWg^)7+jun;`(qH&Yn1P88gL>?OsUOh^?UZ1?~XFF$nZ(O~X)ZD$Ee>{1z zRa3~(3RdIWArxTOV2W;}OXQW1_1AKp@9p~KVee|&CauZwB#=OX^XfbL%jaGt#FRX( zLhCQvHf#|nCrpm|vF>DIS_{^r-zQ6$`GMAVVF6V;_Y2bA8ulZ9Rw;g2&7ELW$fPqZ z&`j~rxY4Zu4%d~OfOS=dW@KI58}cOpp31n?BDV<)hGhP|M5^^}rh)Bbr7|Yhk-cuV zx!(OSjvUHxg0r$7>t|ELuRYQdEq2@49;LTO;vyww9LiP>qf6E->e+!Q@K*ZHGA?~|_Q2G3T0^b2`6+EBpwUDA78ls~FDx)J?HB3zJ z1xxb&+ch$t?XjW=1216ah5eisT==WGHXfd;wCUQm`mrcf><<2cx=s9Wjn*XmIe&6X z*EEe?S8k&$vn7oqzS?hu6}UX*vo}xrsW4%KbPi8MxPY7_#vD>8*7qpZ*Wu+gfdv-8 zyoMprGdQMRs$&Jpym`Uf*^A0R<7sk2c(BA7975LJ4#W`HUQ@Hn`Ag=*wuZf61Y~*hDzu zb&VNET+8=Xp7F8wwDe)&IX^)CJ(CJWV`p- zjPDQn9SPOb`+hV0WLETNMqgmO5LfZXV1|cEadSu?pRZ^Zqrk%?MHFIr%0gchrcop z?f^pE4i&Ln`yBoz-hvqUHI~q#`X75MUasi4zjuSwq%|npF}f~n360atC2NyNWTSF- ziRz;ot9jp0>Z+9I&Snp~P%bN$1XL!WZ27t*Y_@C7B6f=iVY4mF7nhM-z zb!V{7JHAFc&ydL4y>q{D{L_})qb)L)UF#O;l)b9_oDV2;$G7k@>&CE_5voubB08Rhm69VMRp-@XxYx)su#rFKJ}+S)T2ufdS6Y05aDfykyPfg=UA zMz0!G!YBOH^i0RYuV#N@P}!^u*CAA>D#BfY zhl~TS+B?z+{RDebtJ2r|dRJ`sVqeo&YJODp@9T`!{^|yXePb&^xxbRLN#UL5&nDc@?&LD~!3%aHIAOjq8dnbcou_%Tg zZC(UkA-l@hO^}DcQRy)>u1W-%>*x(No(SPRn)AOuZ;MJdp!?+7P=Xu@$nUvo9ixob ztD<4XHnirx^w>QrCB}H3O=I`4ep}?i*U?nb#hMG_o^ET#`e>&9U)n&Z_sby|#%is_ zspXg3%Un*7|EE%@z0&a`Aa_1)@?~7%ms{Xehl|!^r#JxEcZEX{UfvWrJ zDH+n?JBLr^7_uO$iw~Ff{Zg(<_%UPS(!@SSbmD;R!N-$Jo^~5UE27!WdwWcR0BKH( z6GGAzMMpwExuQ>27l)@Sl;Bj^K*at7BBzwAVC!j{T^uioJAKO$M}b|MwbjDWs-<3MZ5K*ZmUK2>w9v;xNr8qn~fC%EOP<6u9%_8e|4!F=OdZ`3kJr_3&F?JMQ{Bs;tXLIb$}ur z`}mV9NnkhUYv7s-*j$w973)h-cIb1^-utkO3-bO{8HawuI`QA?hjCtuHyFDCd+;|Jea-?=S`l2eQT`X+ zujTh?ta^K1#YZP$|IEADmL~`h)7dGpUNFg{pI8#+e8f)W@un;#Y%sIlnp2>l2pd2A z^d$k*F#9m~5+*K&z&lA!hqHkll0c}-SU5IHVukC>|ZuqZoXgC z*^SZ9XKH~xkrN;&X?17MS1_RZ)Ub9Wi|Vt?&N+AdOxwlRP{{!G-O-mQye@xw@S`($OqDb5Qc&3fZ+vwLxv z%1^sw{quYnVNY($7gjZ>!N>tP(0UHOh>;%q_Akl)nDV`U2AJ^Iv)qbQrM_7q7L=dw z&^lzx+pnvf6Ke^Rf76ah01kFg#me4jh#CD|czf8asRvo`{)UYJ_wbjBdpac;^$C`a z7cWIC?SOV5l#g5W0X6q>8NkVNW4Rqs?XpAO`hdRX?^$%XU{u~h2?OuK@LWatIxzzJ zYC+5Zgpr9hv`)FluWo3s=Xq9&uy_aPRg&uw{Zw4Ke4PL7JxE8HCkmd>9^iffPW!&+4BiqP;Cox zM5{r|G`y{r!j*=?e z(gq*Fj5y=ugl|`BpM6{r$GFe?`iKnN80qND7ABgOgHv*I;&1r)DD^o1a7*}T%iI{B zw(H;w58B#`H(LZ`;c?mbeLv%EO~!e-=ukOIy=urhdJD+Rn8wSjSF)!lS%kMi#NxRYh8FWPKlQ5DibGM>BO0V7>lcrCZ1nKb)0bGlD%G1 zKoI3dLW^yr^VEjxyZ6~$`zGJtB7TP3D+@-~hny@6qsLz#+?GX0*mmDU7FZys`#*B1 z_;iHrhMf+PV`qef{c`I*%r)IIn-Z?_8g0ov8y7-f4c-ZB3 z-beWMJ}~=iZFB3sOR%-*ui0fB1WZI?)FlTNVhf!L`wtJl%U8g`F3Gi35DVXQ(t5L_ z^(^=LtsJ<@c0-NI`N8Dk9D6`45-X+Z@jnO;x2l4O2=_D|z$XZ+#9{Dy~gIGF!rcN&+Kl>>x!4&2sj{yxHLWGv0eKh_y`WM%OTDH(S9!y=V!-i{|< zoctWIROVLYd}9JQIdeFl2lzT9Y<3BkDhxbe3eTjE860^X>Z#Z=7Sj>`*;)4|l?@Bx-&&xa!O%zAqY#?x z17nz6yR2vajFnkCj7+NBW4H;mJ>uKZOR*Y6CUR@`;qt5d`Kqql8e|rAnofIR(Zy}C z>f3+BQ8uYCW3dFKo4rd2@W9_cP3};p!vJ)R=1*z5S%BEiL}b86;`%$P~`v`-?P!-)(tF?s&)# z3OyPRe)Vwt{lxI{jJh!|`Wk`-Y1a{9ZAT7Bg6-R}!ryRC_2zlEQn}r)iq=jWKt{l;6Q2drZFz^ps&(*N)tZxFPoLIC ze=$!PToQ~9Yhvs$i84HH2=)C^V9;<|gmfj9e{2ZwtAqIUHv6Q)H*XLxMO`kX-^+X$ z`SmX>k^u67zw8-@%Dqz2TT<8;CRH2{fOZ&~fd2hwrmTiY~ z=|OTmXB)%F!U2f@etzwi_Z-7PN_cA@R6T9@4z~RRk?9Ct6#DXh_dfT0bA{q5yc6pR zCa~ndQCo9A=U^YhwJr;7?OcIvC7wQxY%bhF#clx$4_9IMGuX0$o@_y4{TV1I$LQ`z zrMATYeIf)t&Bryf*Sv=p+N?G@gdQ&Xek;�}2Z6@~Lbs5>iHd<$&z9)nh9wMsd$$ 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

{JJIr4`fvdxmP1VC&W1=4!kUw89aQ!rUzn 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 z z9N_cxv48;T(A=(?SGm6(NdsYf=gtg_Y~)XgmI>4gZoxR|n#$WdvKTBJM;~(ZO&wQK zGxg~oy{@A}lIo9PdX)8?nZKcuPLzwywe!^yIG$7Q`J__6QCghKpo*FXW9%#BdOPEdcN zMnE0nv%e|)QD4k=_l~KpX;Q4<+`M3!9A`IC=f~~j%mgUEQw3O$4{GX25Xi_O6Qp~b z@R+p*e(f{~*JUmO`fD;+UcrlWi{8SuA<3Bx6$|Zc%S>M-T#)9CRmS{O_8@06qub#X z#dJyC<6`fQGT{)Z;LuiA*`uhgF=V0_r-+iOB$&EG(}Gynx5Kf8ZIImBld^zMjyT#z%e6{Mw$)}p z08Dc3B9#WAq9QiJL$PqRZG+ zXg?zxW&To=D+8N|DJR&{gpIyUyZOe#Y!HP!e)J!_nV}UtWC`y#Qv^`-@cwJo*bLn5 zox{ZzP0#G8x=O>e-xGS0VtQlyy)VCZXHCMQ9ogx_fOys(=A~DduGbaGy+KAUA<(hbs#3x(>rR*2anm zI8(5;k~P`6pAevX?U8{w{LsK@gF7pGoTf6vgR1#IeAd53YIm7i~{loV4u0cRu_3yhmMX!Y-8>I_!q zTZ2?P@VH71r~628UW%^~p_3+BsFMk0YfT88vDEK>JiF9C1TKJ!1o6K78x(fK({~1g z1;b4zRhqTqpxD0xZQkBdbh_y7?izA|n*-M6O`x~bsH90IY3pO=-*2>xpCJ$1h2=cm z0r>_dAJtQB;mmI~!ZOgt(m0I)Xk7(*^mFeV_N*s`=moand=_K$qp(D)FGO(7{F_nL!WPY&hM z^6pPZD~Rj#?cNUQ{slz2kn+od|IhE2SoZh1)BE9~hpyyvv}KpNj~QPnvyHBoDKkZz z>s&Db>)4&>87qexJ3i;gy8a^2Yg*r$@pTmMpRzVHfG#?Cu34@f0rk+7(qb()2jZCH zZzu;l>HC#cr*;{V`0}Pjm}YEJ>MSOzBzgt=_no{4!$-(-hxiw>Bi2F5d#V|o9iJTD z+l!b-iipaWw9uD;2w)IP$JYl&vUFG|W){)NrcNKUM^ywEhX8>?#9DY0{cj75QK#t{ z(ya7bfAW}tjunv*$Q}n;O1Pj8F4QYL)rd0O6WpH+!+2RK@~65EtL<0o)pgG9_E!C$ z7jECDZmSya$HYaAAsb`7r*9JrY;Xrh(rJ06Q@340e@a>vZzJ}7F+qTG7(#QG5yC6z0)TrrmovqGSm)zr#F)H zbI9Qo!N%a|QCrutfCr7vWWtvVB@n=#Taba|*TDg=)JDdM^w%%vwK0=OL|@Wrov<{& zh@f>%$2Fb?;C)t-JJg>=6-}Ic>^~O&S!66n+Zvjczy@07k7)9|k-*MYvLb`=DCuOm z_kGiFoRQe~m{8?@OF)Rngh83`>{bzTOjBT8^%r>vassTnowQDI5a1g)&ZJ8_PE;bs zHi=5QNm8>C{R~J7kw7|Twet;*DwNR8J`CjP=LOL>|NHU%j7%kWg&`TYk`A7A{*S2d zaD@8*<3D?olszg%_8yrP(n7McPC{15KAk%%l9h<;l|3WdIkG~Wz317^=s5e_{oZ`O zzu)f-kMtRCpx=7m|e3>K7@ZQY_DHxcS4j{ zPb|U$SM#qBwjOv47Qd2E;}P_?d?o&VSkykh?G1m5MtiMv$Tc4xa{X|2rR<>F$ofVz z=2T{+N9s@Mm$e`Foyat~<24k+24C8}{=@O|PeS*y^$gRus8_|ew+G7U^xul0+ou$h zoV&~KYeJOnOLCC&hc~ENH=X6-s+6rUVkgn9mq3eTAcu?2fcdyenwFX?8e9^!t$KaX z_$g7}FLnbCrJM}^YGuh))NY>4x74Pw{=<#f%Qk;h&785!jH!QgSrCoOXT>!cn9;m) zefZDDD4~*V%Y+q{96+dr|lITC90k0+*tJ`!h{BPY~;{e7JOR-`HR zcQx-L46LRVzMaFAIkQd1%jjqv0?JM>yFJq_Nxqb*PK({&)~cY(-g9{uz)||()(3p2 z-N1uuE4O`1tg%v)0XCju0<&OIn!~SQHx{E$Mbug(qpm~mZL*!>WXg!5I&e;>Zo8XG@%lEgk1p=q+dag+^_ zy|31my3(6d5Gm5t`lHk!&dQ10@ww<)h2#v>(D-t(CDvPvLjO7i#b z!+hUDp9wrdn=E|%{g|lvO5w+^MOww~nt&IxemDW>0Rk0|+Phc18mK=2<2DCq?{wMD z&9D4jzN;|}O;#b{btF1c855JMuFS2i(t)dI(+%{L9EXq7)h7fe)xnT|Kg=Y+#4l@a z9qIEQkf#*oQr;%v46zB1{~7nPT}H%%Pc!fjYmL7R`l0NL7|w=FiiA%@*z^oCaLvh@yl!Dnu?(2 zu8U&2kcF#dsF!IT*AFT$cGWAB!Exemp2YirYI*k68RM1 zFw5fQD3^Jo#*e(q?eN3i9tV@ix4H46<%1uPL_K4bt%g-`j^0!Zch4}oPL=r>kaypS*IJ8pC6mj zY2C3&htY^^>*qUW_3fVCY^Qg>lpYP9#)|C47Uon3Zcq?ERIGb6biDVK^4@Z4vCA#k z-BCLdhi;Tu=Lp6M$~SmIh}Szv{+o6)f2GBFcc^_`PBNp8M!u^8_4Za_;W|n5VWI#_ z7FBd|0m%q!@V9&Ozf1iVTu<}5HXd%oP(|%=qnWST)t0`0j7XiUOLau_^O-boeA-rh zBh9jImtOjVxzK1WnTIsxldG4bX~8U=xjs6{2&%Jz(l4+g#spxut0W}{}tj>3OX^SfzKoTT`r3s>7A_DS!LZ&yuhFk?UB&+ zh1iZu|K^#$b-?Xc;o+ zxNY+z=%_Jd29zvo?SCYFwqc%5JFz3w=Fp2xS0M7W5=lVNiOVmI%(^pi`4b@<{)2G=aB*hHzVlB1g=EZZx8 zp8P3c*~TtWrl*P6n)4M?yb)k}93aTW_3T|~l-Ze$!sC`ZpKnqIDXOLN&0zaP)i?y# z%qVIO+dE?Fi-bVfDa{u(YuDW=8MZ%pK7YEF9(b5OfXEKGLiUs4e+1(xv>~iP8o@El zdk;SjyQpI*LW;JuUr1TGT|WXFO0XaG>B?|sNK;xM9(tqjemC1>c(1Ek&(=+}R--KU zznX-fY}NS_7H`lbc!K+AbT+7+XCgpArf=I~-@x|A&+YrEOt)6Kf5C2T4k@wn z50>0#vWhdoY|}G;)pTrCvL!`m+5TlI;4OxEmGz4q6;4CAp(r;5J!!M<$DrO!TOK7H$YEY%#( zoQ#daCOAgv+Kq-}_~$3-z9C!dD-w1B+P&Qlj@R--2(Xh>>%46A>xB}6G4tl7QxLqV z8GvF?-uFz3l?h2n7iS@<{g=EzCHt9n@0FmLiuS$N)|~v!6s5kG$4eeiL}1ixyG5Kv zN=m0Yv$M9bWh*BQ{sac6*YLlE*NqKm^;=0f7aOEy?yv$_DV z6bPPkkLkdpq9BI35^qv`?1L zGg#K+)G$&cM84|tc%dxx)SxmcFq>ye&9)jb2mxklXMCNx*C+UGn^hvWCY1%tF?UN} z6>as&bG%KmEmQMtobjj7clncAbnf5sEuEejvabqG{wo8zHPm$;ifvECs|)`u^0XxG zCsw{XstAp1|2CGc>F+vZ=RK7w_B%@Dljkfu4`F_VZ&0EU8=iXz;}Nx$gvS7^c^4(f zEoOO|cVj7w=^~=OcjnU5CrgPN{xo_37e=%`stj+=K4YE(@T7FXGc_k_lC+jByxr5~hb+c>SIcB5`)w~uf z{ynOblk}<8h%puSM=AcRuV+m^$SYL~Idf@C@_z}tqkD&~ffe5}a;v5LNAo}<@m+Kv z%lmO=rdNBbX#D&?`LiPt^?Q1`dcjDFmE$sLVUPjPq;=6(Ft6$HJRoJDO(}qWzc?+; zAq+sF=n2Y@F2l7JmRzb+ogHGkr-cNkU!5H3=GH@3_S$lV9JfA9U1hJSqBFAc)UJ}E>R-5oX6dHR$uV){x@uo`}td25^dTcE%` zI{4~0W;jdTUo(Dq@5h@Jv0WULUP#}`F`i`U`UrAv8rwcvyHq-dx9+zFzbeyvGG*Re zKb8-PTq>Nbcyz2 zM=knCG`6M1z0)Q-7k_Im6ds>ke%agJwKu(yZ&y05``Scc>1JH1gzMU;2g2ogDh4&z z)w)LmSeP2Lcw}aqAgj)}BDXV6)$b}16{_a7Wlw(C9;t)C`d~mm>HAcqf&~5?$o#n0 zc1DGH#e;In*=Njq8XEn5d*IW9L!UMDaqyd2uA!0ekryC<+qg7I{Mi=eGY<04uXX)Z zKFrqXUz%>R_KyM`bbK_8a~O2~Sik0#qdZ&Zo{)Vr^ht}W+RdnksSdsd56|jTxnH$6 z-eHN<5l;^t<3rvf;&S-16?hQ?A1pnzh~78?FbY=Xd{x}a8lQV;Ws|FuV<)E~RyQ`E zMKb#RLV3CWdcrrI2RUtaGHjMH5%Ai2uFpx4`>V#FzwbL~dp=2MTwr1QuP zuxjEM67`L=-)gF#ZJ-8gwxP-PKTAu9_U)=DfI;)3^g>q^D80Bo4g_#eqX*BnXkfCA zQAO({r`r6Ge|{F9p_wy0nE>5jy@&tvK-zkrpPbhvb3BUnod@pRkXuV1HuihWUawwg zpz^x^+Gm8h>gV(kwZj5?Zw!myR^Pn=Pvv6EhYTLRCbxbJ9O(pfMC!o#4}FpmE=|(e zTbOqnDz#`Qo6@fybiAV`Hyjxe$7+4692L!F^8Dv&_J!I8PsRXz}~>#_igFLb6Oh(ry?TSXT`{sAJ=aN{P8HP5X<6nkxIl(EFy) zX__Ttn&f+Q+~gIEtI;Az(OI3z---XSwB99CJ@=?$ew1uuO;xD&@CKH5S&waUtgeuM zbiJm7*+KT%$l8{`NLBXB`{@BYhwFmY#|g5TVV{xd-*+l6fzzEY{R6@6&KWKwRVp$9 z*!CusE8y18WPUWaiH!$_)Ds=BvnE-)T?_dH!>f`n3$-aM>LhcD*Z?;31eBhmo5==h zTi5Q=7YN;NP5~L$=6s(K^OA*W-ya8y1DnO9Z3{X~^}49AH-c%|X~3YtZi|=2e_p96 z3peLzbTGy+^~k44WQ&_#DLb_fljh<{s;IINXpbY{M$-rNfELmB@0o6QjY@^TJ#Qtw zaPTA*?^rg)8Cz?XxR}?xOTXVu8W>8AwC@jq`b<=(&t(ld=Z%Zzm+PE4Hk-b}>s9kcxkc}?l4batW(9D`CT6yZPczH~K zep$02fA*~|;K3vh)VPm9iNZ+Gj5x#Z(Hp;;Y6XHf zct?lm3Wcbb50j(kvS9=OYJK?{U*UT3$l5@lblYQuTpv(S{DM9P` zM0XHKgHxhHY%WR>c=;Az?o8r%{wmA=UA5U*rBPT%#nAVt+{o@?Viad<^H%60v4h%hh51F?`Q>Wjsg}pEg>zN$v>uHZ}LJSjsJRw zYnSUqPc0><&gwKE+U1JC<2hja}{# z(?xPNtgL=-uKutjr;Vr-bWXQGdLDuldsWw=qrP~*1|vEs;Jgv&;_~v9Cg0+HKBVA> z*y;BSj^&8q>{Yr#ExY}?>{25G(!vr7iPYp#47K4DqgAmF>-beA6CDnA1H=&gl-Iw7 zbHAoi$>(OU(qY`MQL=4Y{ozLsSgFRQW{cl|1^ZW5NWS>2pf{V_-U8@E|e;@$oJ6;ym`3u&(e0IAmVS$&mX8lPh zG1w}Z@JQ$^&y(V$x@?vy8>r1XPcoV+YjClLpxJY+M3a`}y?fn$`~9@*h}JPAntcOfyC&Vqyt6gfd9i0fzp9siESuzWb$8U76x9Eu zY%O`nS~Qw35n^f|MJ%2rm@TZ5=mXtL7sVRyD|H9p_I+Pm=7z&Rf1{_@sGwPFZ;a`K zo~2!@?&_$#tuYhq)YIZ9l|{BA{RWvFL)wr7&?w5xPwUE9W=I1QdxB?p3e@-Ckl6+_ zE=JIdPzN)QWi$MKh<}iA+DnvHJ+>-6H6oqiyIQ(0>0 zV14D-SsjFf;9T-LMQ&*kNKBz#aqeZ_5x>G7)c0{6JE^CWNUC|3e5s^Gg zfRw=P$?x5o)}Af7Gs_`oKo3lxvy9G=zA}uVkC=32(7p~EaV>su_0p5o{M5plz!x7> zsfA;#_gC}^>yAfK-evlX2P#t`_va$uuL1S&MYIN+)+iAq4Z%WbacE*sEbPIYnA&nX z@kVj|mUX%xQ1Es!WdqItuNx9f-71_RgoK+lISk!GL!^Fq*u0jvX&f)dWzK5AZ*=U21~GK|EHr}(BuaNth8&f8 zeO32;=0sBgo5N^x`Jc6qO$5twUVo2pTDRf|-4!wEF(YKp zWTACOQjps_!*4L`XyhLYqBke)=sGa#@Pb4Oq|1C>Sxdt2s4G3?0G^NP4`0DCBedtf z6X;I*aBPn0+d1o^l5NPc+Kzr0s9F9z2LpC5Z=PBwQ5^(xHCHDAGRhXUxnhiUJ^78( zUgJ3?)yX$ohgmTNT4S4{Vt}dWJAnJmbAk0gGita*Ng+R4Zp{1NQKi;dw>-WK^*GU6 zn?Z+9aFwrp!Fv~zl>gy|eFFZg{(?qS#puZid9{|Vlbb>kG?}X7b~&|QjSHm^G2p(! zx}G30!YV27bJS6cP+Mar9%F@L%HjrSo za=OX}eLq`gGytp`8CT4(d@^oa&EiDpZ_$d{YyYnfyh~vNa*)+uk#gw~nOrKoZY)FM z=otQ8x&3fWFPF%G;D_C3nJCL6@d_iVg%cd3Y}eEy04egma6^jBnu3_fG#IXYC23%X zYc-=*Ups($rdt3vy(J&A?g55<-Hl(p>%qhX2u#PBqfr}Y(fK!*1&5HFWwvbmi5 z>}}Q6UKOt~w~GzrS{3*GiaVW4CMZFDPkOuznjWxBqE5`K51Y!;)d;LtiI?_3@ayNl z2USM}MmB8?&pQ9TT8~knb-9xvF$In&3^|xf7+Y&!ml&mlhvIapkzl<*YvNvMd+|#P z(+Bug@cI1d3q`stx&KnD-_i{(MkN~jw^2T`bC(2R@h+rE^=xAI2s(s8=KEROj}w*p z97wQ0k>KTXFa3RGREK(!d|vpLtYpfy#LwF&ktF7QK0xM^ZxC8<_Aa$a@oZ;p1&66R zl`g_a4DHPcHZ>7WL&*NOZ1)>?`ME`ZzeV-*>$Tu5Qct{jg#X#fG{?)N2S=@Kq4#jz zIED2pbmcDzU*|(LRa_CSZS^-Uj7bBCQBKjHFZ?ch7QK86UbzY}$>pp(4Bq%?!!m+> zHk<4A6!y7E;Y<#4!?#;nC;6AjC1OwCakSZLInfRexG>Vqz^_4q+_-DOP2i38&-0!3 z?4?Q{c)Lzaab@3LJ|w++;8g5qQpF+T^~T<-ZQVi-cSuN@>tI(*r=rebH_#|GS=z zG>*~Nz2a`Duqk=*RynEp8}RL=Pj9-%)<@Re6k5;L$+*rgqmgBItsHg;#Td*LB2c3& zdTw5H=_@i$g?mN!=8T4#W&fWXNYsGwNUK>^w*^T&Eu6};;6?(YJqtFN~Vhc7Pt z&_yabgh+d@06H8tL&;7P(HPa3-!ksvlwh<2^T zml^-}O1|6vg_3f_e$hx<;_oh*d>;@it34A;*NON%UhSaqI1#B=vtF{!*Uy$m6jK!i zM%6wfHtrd%{F4|OLU!2@F9Bx2`vO|Yy!K<$m~RFsj=8z17jd#@h}XpH6k{%RhH1|) zRp{`u3*bJ?iZwkfV?{nH(}h)T?!YA18c$s>pI>ZmGq4>%qVx-sUi`B3l1p3_S{8em zN*SWV#%v%F#o$sz!j>D0gqcbZm1h`WetPbG7fAts9waI_p>1*+T3M3?x=Qb`FQLw14TK+y2B-15?w+eQHuZqc~Z?S@*Z1 zbuNov#v=3QtsGElz*U3J7o zQsMTyqKA6X&XH?;&yG;a)?;4?i>$zwB+1WZwV7MDbM2!Y9yKnWnJ{P22i(v!?vt7k z9a(GGOBQG4zvU(+ku+OF%I$muB13aO#xVYb=&>733KN*Qh^*b%uR@L}XO`qVUk~#cbh7+u@fX zlF@Ysp0Sk+%)02BfB(K3zy2zqMve=zp+)gP+pA*BK*B4<@4A?#{sDHU#}ZO?L=Sa1 zsm%EYtMoDVswE2KkuCL^c$@D}s$3)dFPjCVr zmhWfvTsIxq=h;Mp2cp?JN^7Op*HWDkJE!1QWCbAK;)UB$U=zSbMn3gt0+?unOXUfq z1j+(6^bn@)&E;WXXUs-S#9nlfT^Z4B;7P2tFpBrrqcWI2!u!*y)?T^Gboh_Du7AK8 zU+^(!2nrf8X5Oju*X^8SoH|$nyM}%&EBiw-lT)S_7-1!@rlYrH?~?{=Z>&3l;)^{q zkxhZD*s)b5;F{-eUPQlB-bzZ7ijr23Y_g24^YC1k&C&4JbnD^k2izXA840WGV7ByC zWJJ1A+2MZW!*R8-6AM0J;9f*&0Stz>#8j84)P%j4?++uVq%5cY4v~^bTa*#!9#n-C z9YIg5u+F$`T=a%3A%5nDo6&+=TU>C{7%K%SFUbAtIVhA%LBz(}!^x?eYuzC&zheO7 zZd692B(R6ey3?I#G-F?fY8YKTd1mu};Rzo5`wL}A<^u`b0eLmUQ-orwp(>%~ zv)e#VyTp*lkabZ!e#+DhuQbZChH>CPLQ6Fo$w0bF=XyiAsu-JbVVI{~}Y@_HzHZs!l%;94BN&u=}0Rz8x zfj2Sa#g0i(leG#teB=^^N{IigCIi-CHF~Jatd!A!JHQ>Ta}B|CYnGJ_oyQ@C#FKzC z1-Z8wJ+5ZNp0*l#$!maLa>0UR-qz1>lb!u#|IHcF_}VT+o|UML#kJqKPqBc5L-2jv zLVPiSA#QacMNx+dtW!TvpLGN^5m=cqJyCB>Z@%L@SG%c;yZx5WDStzH2oZfS{&a8c zhy3pl8M2h&y7AY#hf6=+6x|sf4!7dfm5*NHIXS?c8zbu>sDC7_(qxi9KAgVSTJvvN z!x$2^uXgu{3{=g0eo*Elka~xoVbkfN2B5SAgT7}562C$XWcRZ!AD_NWwh{|WwGs}D z*=Cc7{U^(@jV!P^={Y!(xLoaaE8KSQ=XqSTn^NtNLRddN#<45b(?B< zW}a1?g1>O6mwA)f!4Z)gA$8`elo3z`w|k*iMo>bVYT+fMoxt&@xk1b%pz^I|0BAX~ zoSO*q+@We1r}vLm>i^PIQh-Yd*u09skh&tleX|0%*M_znifBGW44b{J_JnDTR)_(lC3Kc-MZC?#^Lil#51<&w5$~@iGmLJv0~p}2Ta?42}4Ht>(3OV_g`A_^Z(*|ieQhc zRN4n-m@zK%B1B-C)`i&U9 z&Bh-J5qJR`&lu`R1IC(X#XX>La<}Qe1RL^cnXbB##EEZ;=aC6BF8p9#LWFP%0w4;+ zdLg-8!b+Ihpr|QE-HqXJ+HEd-7QmC8pN?@G2yfRLRBm5g|JgW1f0#EyaV)?<4IE}e z$N~W{8y8c6o0B*u{etw@^3rA0rEf6uspImNg4DCL?c9iE{l4MpxdFS+5d(7`2PYC3 z2WFr+QUc9{<^zH10cnS`JROl_zSH$2EXa<1u=xw-PvlHX*NP$F$UeLtp9mSa`bLL) z&=O*<2E0e{rb2jYm%+lklQMa2nAtt#Qx)jqi(6^e@uH;XCP?M;V2Z59~P zHZiXm2{zr&+;Ilqo&nsZ?f#mzWi%V`lp6B78Lc2ZY4XU~s@5AQ=x|^@$A-L%LHBe& z|K`8HAN5)@V7S=B^`$dX2D|SV;QH)dmQXEkc{Q2ox7hmXR@Gy1XxWiJEyL_1}aA*+hBV3rJ*JZSJ6;?ShNh8_;QQ_+g*Y_mIrwCbuW7neW=-?qQ9P zC3A&)c!jP1dj+b%1(tJOtk2>8>8~dyj(f!oHl1f@I3qVMcEly+5-!>Fnsgm@_M3}7 ztcioyXX48S0dW{;Xy*gkej3actbgjXp8fm-TxALa7?48xKEnex&VjkmEuaVd4kWWD zGM*dJfA|}1_cbVzO(S@P2vIC>RxS>B)yD6iY!zdjRr7f++k9895Vx%KfhdnVLcO(? zO|!$77!+#&r`IQazPgo)zrSHg8jyzxuMzZO;<~Us_aOs`7qBTnWx_T^$zFB%R5G?| z$^9bU-R{TiIzkGL)(5Ho3FsWd-*S4U<)iADMi!Buk#;ko|0=030lPj|J8dnklQKAsZ^jGB!>4cPj- zi9m2{k$F9?@uyi>(ZvfL2q8N!mYxSDoj)$lpsBv2`x}qpHl4~89Gr@Y7eifhR!#}_ z<~djwKq#IGL39EmdArO5y@@yZoWGyDQEHB28`Z@`Ej+{Z!RvSu&^3yS&u#Wlt^FiN zG|N=bp1Xpr)kzwgkj(dt2Wc@7;h}(bzQLO1&h@l+HM|79L@h#j^BkJczX+f~^7(S^ z_7(!RT3wpF2~dJ{MnZUc9&mBur>A+HN+{R`xHeRm?BPQ{ z10_wmEdPm=^{lmyzw4s_M!NbS|9K9xk6SX5)8E{NxONjEABmy69f^F~>Jm?_)z5Fl z{arI~3_`XeY541YKpQnId{-tbN0E$WQqPH5q21(-*IJ0cRWB$J&)fsv!zW;eR^P?c#z#8n!_G&9!nZDr z&(rz{s&MwJv`Rk&alBg$uR)5jI``PwjVUjqy~;Oaq;sJ4TGrVnz~Lx-M#Y7|1)m(Q z0P=h@t;w;MuIA0*IZ6Tc2UJW6_?9VBQMR)1xL%A?`ld7fz3dfsURBU{$j_0xlqF=D z%DX%eOEs;XKMp-s#2vfP6O-cXF<8D3ndO_xX*OV8>fuM2bB_U3%y{893vCd7^=^{8 zU%tn5m;_`^s@x~jQ@bUPd*wI`yi=2Rv)?>SIw5UBW2w%qkl=Y)u;Sv!%X^lJx4dq`d>Z@9&&A zuS6K;YdSRqk6~*5K{TC7;^Hv-A%^UnURvRAggqzRvB+cQ*8BGfp(xc@N@rXBc8p0v2Wo~WX}r+HT4D0X?S zVq4?LjGP*F9zZ;oW|g9YT~rq5n^VnMlpFcl*QK?0@pIgD|D2`7{~~H{RK!G?$(HF+ z`5CWV-~W+4g)=(w!Ri5l&9lj?d-xiIBc`i@z&WV^pK_<=c>YHsd6s#m$d@#Kl#JaQ z0Wt2a`0H+-02FTe-hUwR0g0i~IUMSI#IyB0ad0+4J*#G^rW@rJzmt;ud?Dv+!Fr-=K5O^?By40Xa_E?Y?HGKf&8YCnGp1tH1Q9W0hh@ueS2s zTpQa^{J4t7%)t8!6p`Ur79othd$`oF%(dz+rh0-&YK1)yI6KFY`HOe5!xtlMYEdqN zIJA+m}M?w5+rA24N58{l?qA99~!Sc2@R^bTPdh z^%n?~B}1f^Dl6XojeWjt;)%Bw4opewKg|)E2WS6=4eV*5)2;>%)Q0X>-UfU|P&xEm z%Q!O2{7}c{tFPh7yZ*wpx2rlog@Af{hq?;5IDSTvGThnE62VL9Ao3qD&BRL#h3~2l z@*FGa9O-inIqe;tescP|XHEYp5Rl3@VBjc=KgclRRX$9B16BBem}GC5WWr zcgTPYJ|xe|a1XDZAu)j26#?QaT(X&+Pr?t>0z*=y%S(^(og>M}NJk)@Y07=>0&=aA z9lxs~$;gvb6fe)OL5ezA?FJnALahQ!{dQfKO$9=^VQ=624nI;F*@}Ak8DPaK5Y( zo3-|((TX*50XzpghdxQ7hY~ZcJ-*Co+{^=l`dA=zY#@dAfbEdKGV84#4<2!_KRLZXW6$MjzB-~N8rBY zuk0hQBOgd~M-SRUiafoEObmO*1297~ zobmDATs;ImCKm_$<4-7x;BIz@0UyGg52e+WnrdXc+irOxpqlIJnDC|_J7C0ni&qyX zijD0}OixEXrr;i{XS`bYFLYVr%;?w=zJwdF~6$ya*#XF#l?w)btiZ%G;7Il_Q? z&OJMTaWZ_z2)!wDKO-8O$;pV%#nW0A`I50`EKG=WAE&*_Y3!AkJP#1vnKF?0aPC`a z6GNyNg^IchRe;h=@&e{N3yhnJ@;5KH~^RdPLu~^nT^thW%hz5ACkM&C8nW+Kf1x z!~qcI;6F#y{Q&qT+@P&6n>@-E(i}TT{!akdBFLuNBlFJtpoL!*YpG(d zpVJZ(y65)F2Dx@cmfcPflhD{KXd(bzm5-0S_t!LlTm;DUytUB1J8_I3neT4h2IE5- zxh*K;lyRhIKqIWJ7+hw;P}Rp3v4s0nQfmhI8;=U-u;IIU&bG6QyKZ*$<4e!Zv`}ne z0mesKi@e?c6Q7F-@XZz{jF=ryv3-S{Q=IDwobW;IKCEGACoxSAI;dD_AaTFmp}pB% z^KeZ7+GMq~BM+@pfteCZ9eVe6_`fIYdrs5gZPEbEfxA)xHxIYTA0XL&(TB2eB=rjo zj7`f%>Nmxe_Im(M2Ms9@sh(+;6gxc@$)0iE-NW%hOMxhN%2mQu412D6T)KFTcm2hi z4X{<9^Q7H;+|T^ib5XJ%-+X(C2F#1zfOYQB?F1XJ;yv!u-WP8-wA|tqBp)8n@k8Fv z=ISLN22DkI2eD0YS4rxqr70p_k_Iu9S8o7WlW*W`VFqsirNi)nFb6 zyEgkG2Rvq(AkhKEf{%x=7WfvN-;f&7zbv%SY($MX{L`Q>Z?q~0OO+7@0!hmuY543L zOT}3o(AZs*MFy;xk@HVBTYH{B-M;tm%!Y96?k{Xv`X%`STatsy-=D}YhL^fka)PbOzf^ple~s4#$l%RBf_9>%O8l<31NC>vJ3ii+_VvPL z*v-J}X*uNI?CM9vh@fj~=5y+IWvplu}~tus3&0l)YZyV+S8xoE-Ee z>>vQkHg;HNlI&R(?jax5mbVy*E%9)`?>qH*TrTbtHh!>XF0aGERtPi(W54-bBUmz{s z$YuJf`laOg`RJM6^sK&m7fdD3=)hQN(aMSS#453Fz4K(N^J7OxJ?bg`k_g#kR4N3T z?CJWys{`LA1&!;wa_#)ACiw-`3%9tftE29-*>^NsSl1*xTC(XM$gx0bOT|WY+6ieP zE*yEilOvdq9I0PnTl_n{OenKR7`?J6}dDwl}zvnB*h;-&dPRR8i{41=V7?K5Xuc6;Y>)qSKf8`_IoWII)!fNI}C zC@rdYs1YY@HA=Zny+rqqzEmFD<4ML@NkQ|{uD?H4i_nBn3#WaG?o38EpyQAB2A)h{ zEx7iSHpOV!Cr3r4lS@REP>^B0U3Ffj4uf{Lwpi>>MJ|b_(PfZoHSY*DZ=KF5cIj*E zi%p>Zk06kb>D0tgwXLjW!zR9hgQ#_wk5sd6V%NKZ!-@=rI`M{|F1~D3d9kQ&=+~cD`u`M;mK2mR z^c4D7vR~WGA|iX)0lk7owOcMT88b}GF0Zm#O)Q75A1nV0AFUFR_VJT?)$H!x$Y9R% zv+x^pPY1_hO;+(Fp3Xx!O|0osbi0Jn-q~SCU`nzY)%?7#XM#Y3vrc-5n%&2H33oHL zj;rMgWC@_+ufWcxFBO66Ev94}(x02aWGAw*;T{@|a!o>N?2C^a_d{veKYv`@eAry> z(#XTe=7brC=&R4~m|~axR040MMd<-IF}GY}SG)y+CgH_z^vzeo`1`-@RHlWOan zUE`t$>|NE71h!?DOe(C%R@w5Hj5sdB?xO}awX1diH%?jGh@C52!SjU+hFeGH6?PE4 zRHP0`U~)ZYodu6FkEt$HKj(SUS|-<#|6=vbB64#wcMsLEiVj0(V@G#G-}`qkvUf;J z)cS4h=u`>i#TM^QFlrs1THLlu+TMdHshLsOF&Wx^%a$WlcAm)YyK)iZPDP8hp66ky zz+==Pc-?1KCY-t9*umtm=jrmtqZfD!3pU@j5-g923KQJ5>f{?U*P%=3($uK(8LI6k zI(jklXhMdrsCa-rFeSe2&KIo0|;FHEoty zjh}CpzcXcVCH{LkgxIChm)KFaz}u_n<6ktg!+yDs!XR22Luo_B$PPY`xF87r^vBsZsuLQU9-(o%^I4(oRHJjUcYL9J8s5TZ1P-#tPnruVLQ#^ zk|tGSHSDuL!MIOPeA3=JBzUTBjDKqpF*LMj$w{18v`k*So^+x%=C%<*G_@sdW(UAI zrn+aVVROLEv$rBC4m>}|G_{X8v3bpOl_)f8NocLjdpa|&^=X!jw=0w2S z@qT)EzkR+cje+)Y)ol5o_bbqz1P_~fByph&mZG*+5jY7u+v#KnM7#LWol&nh`IJ+| z>Yzq|rZAD?<<;hjRZ@Cyl>8K#IHEK`Tiy7bV(DkhBG2+%5uw#*K9)rO)m$AsNK2em zqLU6wL=SX@=AGgtdXElW@ux0S>=jtP-6M86A=xvKi!upFGU@v7kcqt>CqA)2sZz0> zh=y*a_M4@t>f>Jm+7;F^eMm#L3}9*P1?VD=Uq@TQ%_ljEEZVu9p($ZI$`8`v;0bK$ z*snY3FUI0m_~4e;KyZ5R)R;24-VO<;{;9rgHmy@V((h*{_fvq-R{Cc|t-$teWSP?o zCcNOAzX){EMpGxX#EYmZim8p&$>I2|ryjCVSCAZfZ0Vl>P>5^)C}NLy){xzr>OW$d z7h1Z!KkH~gp*%jgqUBgPET}rC*vxEl?&YmuHM5j)2xVNy{zaSY-^SBrCY6HS0J7^7%G(r zF0(&X=hC~%7rq)kcM4cVksSFgXf>G^4Sk4GDV7Sp&jMjaad<+{aYU1MhJ~Tb=IbHw}Ge8G;g`uMp*QuUIDrKD9eT zD2gO-5ObYFN*`rnsjevmUZyOS92JJgP3B^f?tglJy?K2%thVf^D?i=ut@E+vPv3w1 zq-S^}w%QKU9_S&i+ac0(mi`@IR)~f9=bY|^n%?;WDef7!9r-bcC;P(BMDl^>+hB1G zlzn~^E6+q`VXV;BN_;+xlJj^njUYy&`*x{{aC;%_qrr1-st;Io*`nJQ&;}lUyIe-p z{~qqolp`y05vT}YrV#*A@Dwu=(BE&3(}7)eaX%*`1`gCJSNq~oohE|nENtBgXt7iE z=Vw&^_N7uNbhJW0dGBw9{mUwX_`?t=VoVn-%AkF$aLgfh&RK`)cA*$2&Uqkj>xC$N!jjH;<-#iktDHMr1v+PqC*m!4 zr<{|FDSnh*15i?)yw)@=s*H^&)7Ku3jXL`2r*Rxq=sFdiQhbmJZU5v3t}0?XDB4Ln zX$cGs4?uTyX`}C-!I5jDM<`xC&kp4JEHnymRGoU+4=La&fZ2Q1!?`&z=E`{-7vSS~ z!iI=;_&gW3H=?pVgI)A6IBwaz5C2)LYQc`vX_W6zL_geD-COr_+Wo0$h4=7YL3fBP zyD2NDD}}oOlDkQHetQX%?$Ox0&308f)_H3GB=(`hI>bn9sb41{x;=H7iQ;b|*V$^g zM2E>CK_f+!x77YjucWGAr=tGazG=eG&ca7I$5ilkz!6>yp$4(?=s8w1HR&=?b&+vpIrlVxL4p>Ukm83w> z>rPhCxQB)cU52ac5N0^whsqUq{j-YtiLq!lqN-Jz3Xj_qQMt^8=62bO6N1vV{ZWSw z6XUN8O}$D1w#6KDrsDH=!fDf|BZ^!oU8HtW+NS!BcXCoDCsED2;82gJ@``i4|KYx_8++(szfbX<+w(8A?=30aHD+@!LRp8veh;LX0<#IxEuV48VuquBUc znT6l&Xv37-+tHnZ0PyJsHaOuWT!wiAnU1(%PeFgqUvw2hMId|YL;u_0v7gVacG?*{ z%#T&Eqi1}P8p|~uZQBJr&pNQ^9uL1A7VJ~3IrgoG-$Hlr^`wKJ#*DvsicpG{8={9) z@ZP07Q*h?+*;$qUDK6OxrChU#$^FT?wQue7W&ktU{(suL_OPU~wZBokWSUx2sd>q- znNDSDUQo;Nl8!ZwF_w;(Levx90@2h^OF_oIv5T6WQ)y{xb<*+%5qZDiDX_%Uju%Ws zN-Y)1Yk-KrS_TFpl^}g%;pusP|3-;=6haQ_+oONZ~{K#laL5k>9>UHnsLNIWyxYIddA0ILC zMB{O2O8<*{)~Lg!A3clZsH;1-5Fh8bW zTFmPH_=d`4GY3!Y*!g*+4tLC>;xJ!yIxF7;s=c8~*`uImvtdQXVp`dzo0lkT8VBf@ zC@oz3ymEZ19#O;j>qMgw9cgq zT&f5lJ&SbKhIV7-Cv*lXW4K=AGlsW72qybFsFq-(^b(0&&)QO2Cr!TeAF|o;WGd=M z(|&(qKsS#8tDD~n9c;0QCNNIwZwd?2em0vv zNV|mdl>5ROZ}SZs7nGlxXl<=WQt`Dtl*ZH1_Y0?3k+QGho2l*gB8jg zzqWRaXVu!l$f-aV%|Y=RchfR&g{bvrw9kf?jI~$Kby7AyyZqK+yHFdVAr9OoQy5%7lT}$lnAjHkq?j z%f4;56U~nqkDdXHL(&Y>;s%Pncmvmi!cc1)wQ6aHu{uUQJ2?4I%Z59RVyC=JYhu>3 zoI5%so0F>7#;g+mK3xHV=PT8|ZX?k`8a5zq9TrIkfPS=5?Xl_N^I8%*Ln)(ZH?4#j za#(fCEV;+F#0jOMsUs6LZj@Nydy)6Z3WGxnVC)=>0|&&=%gM`8_xE*}cJIwHuszY? zLVT1(d^aGQ3y~fq`*atY`I|lQEB$N%rIv%tuEz~NyV5eh{60mbu12-xn_ro9OBz&Y zJwd@ikook|hxWKcaX7k|N>Qc_T%d}l3TYNk_Ud=%GL`w;fcWnzx~Hb6e?Oq>vf5MX zsK^)XCls!$dxpayMoqFVETF%!ZZ0_?0I?Cme4B)ns-&J~qL~>eEmh@Ewfwd@8HE#Z zky7?PD`}--?$(C?uZr*cSoEZaE_QnGfRa&UoI*B4aC1L~)B!U&-W>)lRa0P$=eQ7K zHOJBjAAj|sc|LBcuc@W2`N%p8pg}olIx?aL#}!YVT~5AV zfJvgHcVO4cuVR?(%aap3CxL6YtG8)=p`tvkZC-_mDEVEmX&D$nbWXU&GhtZK4`Oed zsm15h9?@ctig$^DA#MRu>i;BP6?Wtbu<$4Im~&jP4FDL#Z2y0 zl4W?F1M{7L`Z8!xnGTe)^=~yfO5t?e@Jd9R@cP3T8F%DH5Pf#^|9C7iM}q+q>3>Cg zk^S-+ZMdEJhcnucJRxzct*pJfVl|h7zCfB-%e`Uk>YhP5{q_?fy3N#|Irrw)V92d% zQRm(XN3`L2!bLxkpro=!gR(!dCi{18<&5A9JS4M+(N%_j*Q2gOOE^@p<1K^p3u zJ88iW8R+2%!oKr<1aYNcZC7_*NRCY%`ECoA>3e8tUmo!vC~U{#C_)by`Vq{0JU# zG@${4HsjY*$w6%kXXvw`Ma8#U2tS|Gxe~uNQ_6j8th6jBICaYlEOcn?(gClqARPpF z$~o6P6EQz9x{?C=ZL1HjwakqQm!mPZ_EZ?^_`ZP(L*d}FE$u?mtg&)vY$%CUts*qY zJWH^0O7Nf8&gjR4 zaO>txh3{Xm>iTO5{Fo!rLU)JNDhW2_?EnIT=wK5@D`GIr5nY~~aLR!%9(^uf$FqFT zO_Z3dJJqOdIm0}WwUcO~h(-{Lmik@YU&YI95Q=O#^g0(1_4q(9@(`xpNiVbKrM%Gk zWulIH%=}DgqDk669(rZl6>ch- zd3jpGDRkhMwso2wEsAut(xX0gqM3I2+E7v;>reG$0;rB6dXp)bMm^ZrNCSQ;(CZJs z0277JR6m_k2gB5A8zmp()n+*Wc-aj>N9MMvIU?GSmJDHim4j&Y27%bbf5alar_$z% zFU}O6X_4#l??>d5O@G(KLi`94F!a#JEC2W89WoV_;C709EWXMV@`OxX+4+=E5~u#hde4M z)<8i6cfhg%=BTA-31C86YVBvY!cBh!6k;@|zs4;t@1C+jvXFr;KbBGIpup$7dx}%Q z$bgGy#N`0v@~+jqTes_yX6HG+vaR?sfnD5+9F_1v`qEoX#cV%{Ed_jMWFVL@r}PZf zUKl??8VrcWY`B_R%1}-{`g1{7c_Nq}}rP zX-|ta3Z6rO zp+WornWY_BG@nxq5)vrP7m$*}2SU`ts;ep-d2Dyh^Y26P5}7k1d;b)bJ=*12GG-V2 zKcNDEvnuFM^}YfD!zFPu8s5M37z$-mnxl9qwRNXw^O;8cBa*d(!#0rTF3|5DU1PN{ z6ROkDD$Hg$O&&$=5am)zV_)O9d(YaB7|~bFK|Ox@SFm&=&xSsQXPIJAjIOb05%J&T z+w%b|R6VU&al6G17Z?Wwg9ISw)HiUHVWj^VhvL^aCk{GNa8`_ydSC8lELn@Rpe8_p zkA`|DZvNSr&K8sX)_;sV#2m2fI#4`fw`-Nw0MC=VLr|~}=H zO(4fRtTTE8gbkXB6hEeRC@Qe68V@V62i+e~-R$(!l-t)eZ>N?0$eGbjg&qFv%g&w* zps8#yNMj6M&&7pQ$97|d)6OrMb077~hJh*{gH?B~(Lzvkal;a`$rMgvYmhtq{H_Ic z=-DqoAv3jS8{{8|EfYAdWW)LWZT~H!Lm4orwivW|n*MgYx`?m8UFWM=%0Y}Hdp?eV zkVBrJGUn+NOmmAAlZtc+{2)y@2(QC|=bQ#XdsK7~SA7RBO_-7B7ueb-sRE1Sl7E1p z=P&xMEY~ID{#`lxzm+J}#X!qZi9G+${7RxSH~wr&-AACX`=DG)Cg2T2V@~a9z4mwJ zUQn%sdVxQ>i{aI%jSb9puog8SPX`#}c^MhDonZwE9nhCe5Ok)P6R)EYe4MWTC)xZ! zt45twF{cF?fmA0FI!t=O4+X?2G-Uumd7is(9vcKQ2eBhV=OFHUnw#msxH=RMbigRK z#@=kSGa3LJipJZ3-#J)pCCAVpZV3i8sQyC2?;O}`?q@sDBM_eI2<-;@27=FPvg!!g z?^Yejy4tF*Z>M&v#PCfTWR)1c&5duCqM8QZ%z>;D!~eq^lom^eb`3EcM1LZ!ug|YP JQM@9v{|hxV!SDb8 From 640db354564c1bd828a177a8571464714bda132c Mon Sep 17 00:00:00 2001 From: Little709 Date: Wed, 26 Feb 2025 14:21:42 +0100 Subject: [PATCH 32/93] fix: Update nl.json (#565) --- translations/nl.json | 58 ++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/translations/nl.json b/translations/nl.json index 7ab85468..7f8dde50 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -9,13 +9,13 @@ "login_button": "Aanmelden", "quick_connect": "Snel Verbinden", "enter_code_to_login": "Vul code {{code}} in om aan te melden", - "failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten", + "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", "got_it": "Begrepen", - "connection_failed": "Verbinding gefaald", + "connection_failed": "Verbinding mislukt", "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", - "change_server": "Verander server", - "invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord", + "change_server": "Server wijzigen", + "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", @@ -42,7 +42,7 @@ "continue_watching": "Verder Kijken", "next_up": "Volgende", "recently_added_in": "Recent toegevoegd in {{libraryName}}", - "suggested_movies": "Voorgestelde Films", + "suggested_movies": "Voorgestelde films", "suggested_episodes": "Voorgestelde Afleveringen", "intro": { "welcome_to_streamyfin": "Welkom bij Streamyfin", @@ -56,7 +56,7 @@ "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", "done_button": "Gedaan", - "go_to_settings_button": "Go naar instellingen", + "go_to_settings_button": "Ga naar instellingen", "read_more": "Lees meer" }, "settings": { @@ -82,7 +82,7 @@ "media_controls": { "media_controls_title": "Media Bedieningen", "forward_skip_length": "Duur voorwaarts overslaan", - "rewind_length": "Duur terugspeolen", + "rewind_length": "Duur terugspoelen", "seconds_unit": "s" }, "audio": { @@ -96,7 +96,7 @@ "subtitles": { "subtitle_title": "Ondertitels", "subtitle_language": "Ondertitel taal", - "subtitle_mode": "Ondertitle Modus", + "subtitle_mode": "Ondertitelmodus", "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", "subtitle_size": "Ondertitel Grootte", "subtitle_hint": "Stel ondertitel voorkeuren in.", @@ -108,7 +108,7 @@ "Smart": "Slim", "Always": "Altijd", "None": "Geen", - "OnlyForced": "Alleen Geforceeerd" + "OnlyForced": "Alleen Geforceerd" } }, "other": { @@ -131,18 +131,18 @@ "safe_area_in_controls": "Veilig gebied in bedieningen", "show_custom_menu_links": "Aangepaste menulinks tonen", "hide_libraries": "Verberg Bibliotheken", - "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheek tab en hoofdpagina onderdelen.", + "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", "disable_haptic_feedback": "Haptische feedback uitschakelen", "default_quality": "Standaard kwaliteit" }, "downloads": { "downloads_title": "Downloads", "download_method": "Download methode", - "remux_max_download": "Remux max download", + "remux_max_download": "Maximale Remux-download", "auto_download": "Auto download", "optimized_versions_server": "Geoptimaliseerde server versies", "save_button": "Opslaan", - "optimized_server": "Geoptimailseerde Server", + "optimized_server": "Geoptimaliseerde Server", "optimized": "Geoptimaliseerd", "default": "Standaard", "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", @@ -161,7 +161,7 @@ "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", "save_button": "Opslaan", "clear_button": "Wissen", - "login_button": "Aannmelden", + "login_button": "Aanmelden", "total_media_requests": "Totaal aantal mediaverzoeken", "movie_quota_limit": "Limiet filmquota", "movie_quota_days": "Filmquota dagen", @@ -171,7 +171,7 @@ "unlimited": "Ongelimiteerd" }, "marlin_search": { - "enable_marlin_search": "Marlin Search inschakeln ", + "enable_marlin_search": "Marlin Search inschakelen ", "url": "URL", "server_url_placeholder": "http(s)://domein.org:poort", "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", @@ -205,7 +205,7 @@ "system": "Systeem" }, "toasts": { - "error_deleting_files": "Fout bij het verwijden van bestanden", + "error_deleting_files": "Fout bij het verwijderen van bestanden", "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", "connected": "Verbonden", @@ -237,7 +237,7 @@ "methods": "Methoden", "toasts": { "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", - "deleted_all_movies_successfully": "Alle filns succesvol verwijderd!", + "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", @@ -280,18 +280,18 @@ "recent_requests": "Recent Aangevraagd", "plex_watchlist": "Plex Kijklijst", "trending": "Trending", - "popular_movies": "Populaire Films", + "popular_movies": "Populaire films", "movie_genres": "Film Genres", - "upcoming_movies": "Aankomende Movies", + "upcoming_movies": "Aankomende films", "studios": "Studios", "popular_tv": "Populaire TV", "tv_genres": "TV Genres", - "upcoming_tv": "Opkomend TV", + "upcoming_tv": "Aankomende TV", "networks": "Netwerken", "tmdb_movie_keyword": "TMDB Film Trefwoord", - "tmdb_movie_genre": "TMDB Film Genre", + "tmdb_movie_genre": "TMDB Filmgenres", "tmdb_tv_keyword": "TMDB TV Trefwoord", - "tmdb_tv_genre": "TMDB TV Genre", + "tmdb_tv_genre": "TMDB TV-Genres", "tmdb_search": "TMDB Zoeken", "tmdb_studio": "TMDB Studio", "tmdb_network": "TMDB Netwerk", @@ -303,9 +303,9 @@ "no_results": "Geen resultaten", "no_libraries_found": "Geen bibliotheken gevonden", "item_types": { - "movies": "films", - "series": "series", - "boxsets": "box sets", + "movies": "Films", + "series": "Series", + "boxsets": "Boxsets", "items": "items" }, "options": { @@ -345,7 +345,7 @@ "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", "message_from_server": "Bericht van de server", "video_has_finished_playing": "Video is gedaan met spelen!", - "no_video_source": "Geen video bron...", + "no_video_source": "Geen videobron...", "next_episode": "Volgende Aflevering", "refresh_tracks": "Tracks verversen", "subtitle_tracks": "Ondertitel Tracks:", @@ -372,7 +372,7 @@ "audio": "Audio", "subtitles": "Ondertitel", "show_more": "Toon meer", - "show_less": "Toon minden", + "show_less": "Toon minder", "appeared_in": "Verschenen in", "could_not_load_item": "Kon item niet laden", "none": "Geen", @@ -417,7 +417,7 @@ "details": "Details", "status": "Status", "original_title": "Originele titel", - "series_type": "Serie Type", + "series_type": "Serietype", "release_dates": "Verschijningsdatums", "first_air_date": "Eerste uitzenddatum", "next_air_date": "Volgende uitzenddatum", @@ -440,12 +440,12 @@ "appearances": "Verschijningen", "toasts": { "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test gefaald. Probeer opnieuw.", + "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", "issue_submitted": "Probleem ingediend!", "requested_item": "{{item}} aangevraagd!", "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", - "something_went_wrong_requesting_media": "Er ging iets iets mis met het aavragen van media!" + "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" } }, "tabs": { From a5463d783de5c4dd0104c95af2d4934036c39dc6 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Wed, 26 Feb 2025 19:25:20 +0100 Subject: [PATCH 33/93] fix: use correct url on save for optimized --- app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx index 988651f0..8ffc2fc7 100644 --- a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx @@ -38,7 +38,7 @@ export default function page() { }); return await getStatistics({ - url: settings?.optimizedVersionsServerUrl, + url: updatedUrl, authHeader: api?.accessToken, deviceId: getOrSetDeviceId(), }); From 446439c2e0546be1e16538b46ea10f90c4c120aa Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 28 Feb 2025 00:11:03 +0100 Subject: [PATCH 34/93] Update package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 074e60bd..96d743f9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "android:tv": "EXPO_TV=1 expo run:android", "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", - "prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; bun run prebuild:tv", "test": "jest --watchAll", "lint": "expo lint", "postinstall": "patch-package" From 8cb10d10620d434d0bf47ebd789827638b2c0232 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sat, 1 Mar 2025 09:37:50 +0100 Subject: [PATCH 35/93] Update _layout.tsx --- app/(auth)/(tabs)/(home)/_layout.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index ddcd6f7b..6c111607 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -78,12 +78,6 @@ export default function IndexLayout() { title: "", }} /> - Date: Sun, 2 Mar 2025 12:21:24 -0500 Subject: [PATCH 36/93] fix: Advanced request options not saving #543 --- .../jellyseerr/page.tsx | 13 +- components/common/Dropdown.tsx | 10 +- components/jellyseerr/RequestModal.tsx | 179 +++++++++--------- 3 files changed, 104 insertions(+), 98 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index 52e64bc5..5cfe68cf 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -61,6 +61,7 @@ const Page: React.FC = () => { const [issueType, setIssueType] = useState(); const [issueMessage, setIssueMessage] = useState(); + const [requestBody, _setRequestBody] = useState(); const advancedReqModalRef = useRef(null); const bottomSheetModalRef = useRef(null); @@ -111,6 +112,11 @@ const Page: React.FC = () => { } }, [jellyseerrApi, details, result, issueType, issueMessage]); + const setRequestBody = useCallback((body: MediaRequestBody) => { + _setRequestBody(body) + advancedReqModalRef?.current?.present?.(); + }, [requestBody, _setRequestBody, advancedReqModalRef]) + const request = useCallback(async () => { const body: MediaRequestBody = { mediaId: Number(result.id!!), @@ -122,7 +128,7 @@ const Page: React.FC = () => { }; if (hasAdvancedRequestPermission) { - advancedReqModalRef?.current?.present?.(body); + setRequestBody(body) return; } @@ -255,7 +261,7 @@ const Page: React.FC = () => { refetch={refetch} hasAdvancedRequest={hasAdvancedRequestPermission} onAdvancedRequest={(data) => - advancedReqModalRef?.current?.present(data) + setRequestBody(data) } /> )} @@ -269,14 +275,17 @@ const Page: React.FC = () => { { + _setRequestBody(undefined) advancedReqModalRef?.current?.close(); refetch(); }} + onDismiss={() => _setRequestBody(undefined)} /> { title: string | ReactNode; label: string; onSelected: (...item: T[]) => void; - multi?: boolean; + multiple?: boolean; } const Dropdown = ({ @@ -30,7 +30,7 @@ const Dropdown = ({ title, label, onSelected, - multi = false, + multiple = false, ...props }: PropsWithChildren & ViewProps>) => { if (Platform.isTV) return null; @@ -72,7 +72,7 @@ const Dropdown = ({ > {label} {data.map((item, idx) => - multi ? ( + multiple ? ( keyExtractor(s) == keyExtractor(item)) @@ -80,7 +80,7 @@ const Dropdown = ({ : "off" } key={keyExtractor(item)} - onValueChange={(next, previous) => + onValueChange={(next: "on" | "off", previous: "on" | "off") => { setSelected((p) => { const prev = p || []; if (next == "on") { @@ -92,7 +92,7 @@ const Dropdown = ({ ), ]; }) - } + }} > {titleExtractor(item)} diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx index ec1ac97b..6ff70030 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -15,18 +15,22 @@ import { useTranslation } from "react-i18next"; interface Props { id: number; title: string, + requestBody?: MediaRequestBody, type: MediaType; isAnime?: boolean; is4k?: boolean; onRequested?: () => void; + onDismiss?: () => void; } const RequestModal = forwardRef>(({ id, title, + requestBody, type, isAnime = false, onRequested, + onDismiss, ...props }, ref) => { const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr(); @@ -39,8 +43,6 @@ const RequestModal = forwardRef(); - const {data: serviceSettings} = useQuery({ queryKey: ["jellyseerr", "request", type, 'service'], queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'), @@ -98,16 +100,14 @@ const RequestModal = forwardRef modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined, - [modalRequestProps?.seasons] + () => requestBody?.seasons?.length ? t("jellyseerr.season_x", {seasons: requestBody?.seasons}) : undefined, + [requestBody?.seasons] ); const request = useCallback(() => {requestMedia( @@ -117,12 +117,12 @@ const RequestModal = forwardRef t.id), - ...modalRequestProps, + ...requestBody, ...requestOverrides }, onRequested ) - }, [modalRequestProps, requestOverrides, defaultProfile, defaultFolder, defaultTags]); + }, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]); const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`; @@ -131,7 +131,7 @@ const RequestModal = forwardRef setModalRequestProps(undefined)} + onDismiss={onDismiss} handleIndicatorStyle={{ backgroundColor: "white", }} @@ -146,89 +146,86 @@ const RequestModal = forwardRef } > - {(data) => { - setModalRequestProps(data?.data as MediaRequestBody) - return - - - {t("jellyseerr.advanced")} - {seasonTitle && - {seasonTitle} - } - - - {(defaultService && defaultServiceDetails && users) && ( - <> - item.name} - placeholderText={defaultProfile.name} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.quality_profile")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - profileId: item?.id - })) - } - title={t("jellyseerr.quality_profile")} - /> - item.id.toString()} - label={t("jellyseerr.root_folder")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - rootFolder: item.path - }))} - title={t("jellyseerr.root_folder")} - /> - item.label} - placeholderText={defaultTags.map(t => t.label).join(",")} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.tags")} - onSelected={(...item) => - item && setRequestOverrides((prev) => ({ - ...prev, - tags: item.map(i => i.id) - })) - } - title={t("jellyseerr.tags")} - /> - item.displayName} - placeholderText={jellyseerrUser!!.displayName} - keyExtractor={(item) => item.id.toString() || ""} - label={t("jellyseerr.request_as")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - userId: item?.id - })) - } - title={t("jellyseerr.request_as")} - /> - - ) - } - - + + + + {t("jellyseerr.advanced")} + {seasonTitle && + {seasonTitle} + } - - }} + + {(defaultService && defaultServiceDetails && users) && ( + <> + item.name} + placeholderText={requestOverrides.profileName || defaultProfile.name} + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.quality_profile")} + onSelected={(item) => + item && setRequestOverrides((prev) => ({ + ...prev, + profileId: item?.id + })) + } + title={t("jellyseerr.quality_profile")} + /> + item.id.toString()} + label={t("jellyseerr.root_folder")} + onSelected={(item) => + item && setRequestOverrides((prev) => ({ + ...prev, + rootFolder: item.path + }))} + title={t("jellyseerr.root_folder")} + /> + item.label} + placeholderText={defaultTags.map(t => t.label).join(",")} + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.tags")} + onSelected={(...selected) => + setRequestOverrides((prev) => ({ + ...prev, + tags: selected.map(i => i.id) + })) + } + title={t("jellyseerr.tags")} + /> + item.displayName} + placeholderText={jellyseerrUser!!.displayName} + keyExtractor={(item) => item.id.toString() || ""} + label={t("jellyseerr.request_as")} + onSelected={(item) => + item && setRequestOverrides((prev) => ({ + ...prev, + userId: item?.id + })) + } + title={t("jellyseerr.request_as")} + /> + + ) + } + + + + ); }); From 9b1dd0923a14e1acf2dae894730217cbf3282a90 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:38:58 -0500 Subject: [PATCH 37/93] fix: Don't show all seasons numbers in request modal [Jellyseerr] #580 --- components/jellyseerr/RequestModal.tsx | 7 ++++++- translations/de.json | 2 +- translations/en.json | 2 +- translations/es.json | 2 +- translations/fr.json | 2 +- translations/it.json | 2 +- translations/ja.json | 2 +- translations/nl.json | 2 +- translations/tr.json | 2 +- translations/zh-CN.json | 2 +- translations/zh-TW.json | 2 +- 11 files changed, 16 insertions(+), 11 deletions(-) diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx index 6ff70030..192d2d83 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -106,7 +106,12 @@ const RequestModal = forwardRef requestBody?.seasons?.length ? t("jellyseerr.season_x", {seasons: requestBody?.seasons}) : undefined, + () => { + if (requestBody?.seasons && requestBody?.seasons?.length > 1) { + return t("jellyseerr.season_all") + } + return t("jellyseerr.season_number", {season_number: requestBody?.seasons}) + }, [requestBody?.seasons] ); diff --git a/translations/de.json b/translations/de.json index 18c3de25..50995e15 100644 --- a/translations/de.json +++ b/translations/de.json @@ -433,7 +433,7 @@ "tags": "Tags", "quality_profile": "Qualitätsprofil", "root_folder": "Root-Ordner", - "season_x": "Staffel {{seasons}}", + "season_all": "Season (all)", "season_number": "Staffel {{season_number}}", "number_episodes": "{{episode_number}} Episodes", "born": "Geboren", diff --git a/translations/en.json b/translations/en.json index 615c3e40..ca19a136 100644 --- a/translations/en.json +++ b/translations/en.json @@ -437,7 +437,7 @@ "tags": "Tags", "quality_profile": "Quality Profile", "root_folder": "Root Folder", - "season_x": "Season {{seasons}}", + "season_all": "Season (all)", "season_number": "Season {{season_number}}", "number_episodes": "{{episode_number}} Episodes", "born": "Born", diff --git a/translations/es.json b/translations/es.json index 8883c2be..c03c2e69 100644 --- a/translations/es.json +++ b/translations/es.json @@ -433,7 +433,7 @@ "tags": "Etiquetas", "quality_profile": "Perfil de calidad", "root_folder": "Carpeta raíz", - "season_x": "Temporada {{seasons}}", + "season_all": "Season (all)", "season_number": "Temporada {{season_number}}", "number_episodes": "{{episode_number}} episodios", "born": "Nacido", diff --git a/translations/fr.json b/translations/fr.json index f197494d..e9c576e6 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -434,7 +434,7 @@ "tags": "Tags", "quality_profile": "Profil de qualité", "root_folder": "Dossier racine", - "season_x": "Saison {{seasons}}", + "season_all": "Season (all)", "season_number": "Saison {{season_number}}", "number_episodes": "{{episode_number}} épisodes", "born": "Né(e) le", diff --git a/translations/it.json b/translations/it.json index c9326a7d..d88ec71e 100644 --- a/translations/it.json +++ b/translations/it.json @@ -433,7 +433,7 @@ "tags": "Tag", "quality_profile": "Profilo qualità", "root_folder": "Cartella radice", - "season_x": "Stagione {{seasons}}", + "season_all": "Season (all)", "season_number": "Stagione {{season_number}}", "number_episodes": "{{episode_number}} Episodio", "born": "Nato", diff --git a/translations/ja.json b/translations/ja.json index 2f43f5ae..616067d5 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -432,7 +432,7 @@ "tags": "タグ", "quality_profile": "画質プロファイル", "root_folder": "ルートフォルダ", - "season_x": "シーズン{{seasons}}", + "season_all": "Season (all)", "season_number": "シーズン{{season_number}}", "number_episodes": "エピソード{{episode_number}}", "born": "生まれ", diff --git a/translations/nl.json b/translations/nl.json index 7f8dde50..7912e6b4 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -433,7 +433,7 @@ "tags": "Labels", "quality_profile": "Kwaliteitsprofiel", "root_folder": "Hoofdmap", - "season_x": "Seizoen {{seasons}}", + "season_all": "Season (all)", "season_number": "Seizoen {{season_number}}", "number_episodes": "{{episode_number}} Afleveringen", "born": "Geboren", diff --git a/translations/tr.json b/translations/tr.json index 7b3a2320..7bc2e6d3 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -432,7 +432,7 @@ "tags": "Etiketler", "quality_profile": "Kalite Profili", "root_folder": "Kök Klasör", - "season_x": "Sezon {{seasons}}", + "season_all": "Season (all)", "season_number": "Sezon {{season_number}}", "number_episodes": "Bölüm {{episode_number}}", "born": "Doğum", diff --git a/translations/zh-CN.json b/translations/zh-CN.json index b501cef0..e3a834a6 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -432,7 +432,7 @@ "tags": "标签", "quality_profile": "质量配置文件", "root_folder": "根文件夹", - "season_x": "第 {{seasons}} 季", + "season_all": "Season (all)", "season_number": "第 {{season_number}} 季", "number_episodes": "{{episode_number}} 集", "born": "出生", diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 21800640..f36174fd 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -432,7 +432,7 @@ "tags": "標籤", "quality_profile": "質量配置文件", "root_folder": "根文件夾", - "season_x": "第 {{seasons}} 季", + "season_all": "Season (all)", "season_number": "第 {{season_number}} 季", "number_episodes": "{{episode_number}} 集", "born": "出生", From 951158bcd3be33dadb2e1a1d415eaf80e57c12d2 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:07:14 -0500 Subject: [PATCH 38/93] fix: Discover page key collisions #581 - add uniqBy for jellyseerr results - add missing key in MovieTvSlide.tsx --- components/jellyseerr/JellyseerrIndexPage.tsx | 22 +++++++++++-------- .../jellyseerr/discover/MovieTvSlide.tsx | 9 ++++++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index cd093deb..55b45b80 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -21,6 +21,7 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; import { useTranslation } from "react-i18next"; +import {uniqBy} from "lodash"; interface Props extends ViewProps { searchQuery: string; @@ -77,25 +78,28 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { const jellyseerrMovieResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === MediaType.MOVIE - ) as MovieResult[], + uniqBy( + jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[], + "id" + ), [jellyseerrResults] ); const jellyseerrTvResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === MediaType.TV - ) as TvResult[], + uniqBy( + jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], + "id" + ), [jellyseerrResults] ); const jellyseerrPersonResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === "person" - ) as PersonResult[], + uniqBy( + jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[], + "id" + ), [jellyseerrResults] ); diff --git a/components/jellyseerr/discover/MovieTvSlide.tsx b/components/jellyseerr/discover/MovieTvSlide.tsx index 723658c8..c3d9d690 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/jellyseerr/discover/MovieTvSlide.tsx @@ -10,6 +10,7 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; import {ViewProps} from "react-native"; +import {uniqBy} from "lodash"; const MovieTvSlide: React.FC = ({ slide, ...props }) => { const { jellyseerrApi } = useJellyseerr(); @@ -57,7 +58,11 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => }); const flatData = useMemo( - () => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), + () => + uniqBy( + data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), + "id" + ), [data] ); @@ -74,7 +79,7 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => fetchNextPage() }} renderItem={(item) => - + } /> ) From dd65505f7fdf3e6d61af359849d050110ef2a4da Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 3 Mar 2025 01:04:49 -0500 Subject: [PATCH 39/93] feat: [Jellyseerr] Show recent requests #324 - Added recent requests slide - updated JellyseerrPoster.tsx to handle more options --- .../jellyseerr/page.tsx | 21 ++- components/GenreTags.tsx | 9 +- components/Ratings.tsx | 14 +- components/common/JellyseerrItemRouter.tsx | 11 +- components/jellyseerr/discover/Discover.tsx | 3 + .../discover/RecentRequestsSlide.tsx | 69 +++++++ components/posters/JellyseerrPoster.tsx | 175 +++++++++++++----- components/series/JellyseerrSeasons.tsx | 6 +- components/settings/StorageSettings.tsx | 5 +- constants/Colors.ts | 2 + hooks/useJellyseerr.ts | 58 +++++- translations/de.json | 3 +- translations/en.json | 3 +- translations/es.json | 3 +- translations/fr.json | 3 +- translations/it.json | 3 +- translations/ja.json | 3 +- translations/nl.json | 3 +- translations/tr.json | 3 +- translations/zh-CN.json | 3 +- translations/zh-TW.json | 3 +- 21 files changed, 317 insertions(+), 86 deletions(-) create mode 100644 components/jellyseerr/discover/RecentRequestsSlide.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index 5cfe68cf..45318462 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -42,19 +42,21 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; import RequestModal from "@/components/jellyseerr/RequestModal"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); const params = useLocalSearchParams(); const { t } = useTranslation(); - const { mediaTitle, releaseYear, posterSrc, ...result } = + const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } = params as unknown as { mediaTitle: string; releaseYear: number; canRequest: string; posterSrc: string; - } & Partial; + mediaType: MediaType; + } & Partial; const navigation = useNavigation(); const { jellyseerrApi, requestMedia } = useJellyseerr(); @@ -72,7 +74,7 @@ const Page: React.FC = () => { refetch, } = useQuery({ enabled: !!jellyseerrApi && !!result && !!result.id, - queryKey: ["jellyseerr", "detail", result.mediaType, result.id], + queryKey: ["jellyseerr", "detail", mediaType, result.id], staleTime: 0, refetchOnMount: true, refetchOnReconnect: true, @@ -80,7 +82,7 @@ const Page: React.FC = () => { retryOnMount: true, refetchInterval: 0, queryFn: async () => { - return result.mediaType === MediaType.MOVIE + return mediaType === MediaType.MOVIE ? jellyseerrApi?.movieDetails(result.id!!) : jellyseerrApi?.tvDetails(result.id!!); }, @@ -120,7 +122,7 @@ const Page: React.FC = () => { const request = useCallback(async () => { const body: MediaRequestBody = { mediaId: Number(result.id!!), - mediaType: result.mediaType!!, + mediaType: mediaType!!, tvdbId: details?.externalIds?.tvdbId, seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0) @@ -138,7 +140,7 @@ const Page: React.FC = () => { const isAnime = useMemo( () => (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && - result.mediaType === MediaType.TV, + mediaType === MediaType.TV, [details] ); @@ -206,7 +208,7 @@ const Page: React.FC = () => { - + { - {result.mediaType === MediaType.TV && ( + {mediaType === MediaType.TV && ( { requestBody={requestBody} title={mediaTitle} id={result.id!!} - type={result.mediaType as MediaType} + type={mediaType} isAnime={isAnime} onRequested={() => { _setRequestBody(undefined) diff --git a/components/GenreTags.tsx b/components/GenreTags.tsx index 35de54a6..ce907d1b 100644 --- a/components/GenreTags.tsx +++ b/components/GenreTags.tsx @@ -21,14 +21,19 @@ export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], t ); }; -export const Tags: React.FC = ({ tags, textClass = "text-xs", ...props }) => { +export const Tags: React.FC = ({ + tags, + textClass = "text-xs", + tagProps, + ...props +}) => { if (!tags || tags.length === 0) return null; return ( {tags.map((tag, idx) => ( - + ))} diff --git a/components/Ratings.tsx b/components/Ratings.tsx index 64d3d83b..c1c9b060 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -7,6 +7,9 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useQuery } from "@tanstack/react-query"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; +import {useMemo} from "react"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -49,14 +52,17 @@ export const Ratings: React.FC = ({ item, ...props }) => { ); }; -export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({ +export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({ result, }) => { - const { jellyseerrApi } = useJellyseerr(); + const { jellyseerrApi, getMediaType } = useJellyseerr(); + + const mediaType = useMemo(() => getMediaType(result), [result]); + const { data, isLoading } = useQuery({ - queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"], + queryKey: ["jellyseerr", result.id, mediaType, "ratings"], queryFn: async () => { - return result.mediaType === MediaType.MOVIE + return mediaType === MediaType.MOVIE ? jellyseerrApi?.movieRatings(result.id) : jellyseerrApi?.tvRatings(result.id); }, diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index 198d5a45..b132e437 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -9,13 +9,16 @@ import { Permission, } from "@/utils/jellyseerr/server/lib/permissions"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; +import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; interface Props extends TouchableOpacityProps { - result: MovieResult | TvResult; + result: MovieResult | TvResult | MovieDetails | TvDetails; mediaTitle: string; releaseYear: number; canRequest: boolean; posterSrc: string; + mediaType: MediaType; } export const TouchableJellyseerrRouter: React.FC> = ({ @@ -24,6 +27,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, + mediaType, children, ...props }) => { @@ -46,7 +50,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ () => requestMedia(mediaTitle, { mediaId: result.id, - mediaType: result.mediaType, + mediaType, }), [jellyseerrApi, result] ); @@ -67,6 +71,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, + mediaType }, }); }} @@ -83,7 +88,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ key={"content"} > Actions - {canRequest && result.mediaType === MediaType.MOVIE && ( + {canRequest && mediaType === MediaType.MOVIE && ( { diff --git a/components/jellyseerr/discover/Discover.tsx b/components/jellyseerr/discover/Discover.tsx index 6270ad2b..e5a84c1a 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/jellyseerr/discover/Discover.tsx @@ -8,6 +8,7 @@ import {View} from "react-native"; import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import GenreSlide from "@/components/jellyseerr/discover/GenreSlide"; +import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide"; interface Props { sliders?: DiscoverSlider[]; @@ -25,6 +26,8 @@ const Discover: React.FC = ({ sliders }) => { {sortedSliders.map(slide => { switch (slide.type) { + case DiscoverSliderType.RECENT_REQUESTS: + return case DiscoverSliderType.NETWORKS: return case DiscoverSliderType.STUDIOS: diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx new file mode 100644 index 00000000..5ab234d8 --- /dev/null +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import {useQuery} from "@tanstack/react-query"; +import {useJellyseerr} from "@/hooks/useJellyseerr"; +import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; +import {ViewProps} from "react-native"; +import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common"; +import {MediaType} from "@/utils/jellyseerr/server/constants/media"; +import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; + +const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { + const {jellyseerrApi} = useJellyseerr(); + + const { data: details, isLoading, isError } = useQuery({ + queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId], + queryFn: async () => { + + return request.media.mediaType == MediaType.MOVIE + ? jellyseerrApi?.movieDetails(request.media.tmdbId) + : jellyseerrApi?.tvDetails(request.media.tmdbId); + }, + enabled: !!jellyseerrApi, + refetchOnMount: true, + staleTime: 0, + }); + + const { data: refreshedRequest } = useQuery({ + queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id], + queryFn: async () => jellyseerrApi?.getRequest(request.id), + enabled: !!jellyseerrApi, + refetchOnMount: true, + refetchInterval: 5000, + staleTime: 0, + }); + + return ( + details && + ) +} + +const RecentRequestsSlide: React.FC = ({ slide, ...props }) => { + const {jellyseerrApi} = useJellyseerr(); + + const { data: requests, isLoading, isError } = useQuery({ + queryKey: ["jellyseerr", "recent_requests"], + queryFn: async () => jellyseerrApi?.requests(), + enabled: !!jellyseerrApi, + refetchOnMount: true, + staleTime: 0, + }); + + return ( + requests && + requests.results.length > 0 && + !isError && ( + item.id.toString()} + renderItem={(item: NonFunctionProperties) => ( + + )} + /> + ) + ) +}; + +export default RecentRequestsSlide; \ No newline at end of file diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 1c3ce45b..a7c019dd 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -1,28 +1,42 @@ -import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; -import { Text } from "@/components/common/Text"; +import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter"; +import {Text} from "@/components/common/Text"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; -import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; -import { Image } from "expo-image"; -import { useMemo } from "react"; -import { View, ViewProps } from "react-native"; -import Animated, { - useAnimatedStyle, - useSharedValue, - withTiming, -} from "react-native-reanimated"; +import {useJellyseerr} from "@/hooks/useJellyseerr"; +import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest"; +import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import {Image} from "expo-image"; +import {useMemo} from "react"; +import {View, ViewProps} from "react-native"; +import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated"; +import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; +import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker"; +import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import {useTranslation} from "react-i18next"; +import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; +import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; +import {Colors} from "@/constants/Colors"; +import {Tags} from "@/components/GenreTags"; interface Props extends ViewProps { - item: MovieResult | TvResult; + item: MovieResult | TvResult | MovieDetails | TvDetails; + horizontal?: boolean; + showDownloadInfo?: boolean; + mediaRequest?: MediaRequest; } -const JellyseerrPoster: React.FC = ({ item, ...props }) => { - const { jellyseerrApi } = useJellyseerr(); +const JellyseerrPoster: React.FC = ({ + item, + horizontal, + showDownloadInfo, + mediaRequest, + ...props +}) => { + const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr(); const loadingOpacity = useSharedValue(1); const imageOpacity = useSharedValue(0); + const {t} = useTranslation(); const loadingAnimatedStyle = useAnimatedStyle(() => ({ opacity: loadingOpacity.value, @@ -38,27 +52,64 @@ const JellyseerrPoster: React.FC = ({ item, ...props }) => { }; const imageSrc = useMemo( - () => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"), - [item, jellyseerrApi] + () => jellyseerrApi?.imageProxy( + horizontal ? item.backdropPath : item.posterPath, + horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face" + ), + [item, jellyseerrApi, horizontal] ); - const title = useMemo( - () => (item.mediaType === MediaType.MOVIE ? item.title : item.name), - [item] - ); + const title = useMemo(() => getTitle(item), [item]); + const releaseYear = useMemo(() => getYear(item), [item]); + const mediaType = useMemo(() => getMediaType(item), [item]); - const releaseYear = useMemo( - () => - new Date( - item.mediaType === MediaType.MOVIE - ? item.releaseDate - : item.firstAirDate - ).getFullYear(), - [item] - ); + const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal]) + const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal]) const [canRequest] = useJellyseerrCanRequest(item); + const is4k = useMemo( + () => mediaRequest?.is4k === true, + [mediaRequest] + ); + + const downloadItems = useMemo( + () => (is4k ? mediaRequest?.media.downloadStatus4k : mediaRequest?.media.downloadStatus) || [], + [mediaRequest, is4k] + ) + + const progress = useMemo(() => { + const [totalSize, sizeLeft] = downloadItems + .reduce((sum: number[], next: DownloadingItem) => + [sum[0] + next.size, sum[1] + next.sizeLeft], + [0, 0] + ); + + return (((totalSize - sizeLeft) / totalSize) * 100); + }, + [downloadItems] + ); + + const requestedSeasons: string[] | undefined = useMemo( + () => { + const seasons = mediaRequest?.seasons?.flatMap(s => s.seasonNumber.toString()) || [] + if (seasons.length > 4) { + const [first, second, third, fourth, ...rest] = seasons; + return [first, second, third, fourth, t("home.settings.plugins.jellyseerr.plus_n_more", {n: rest.length })] + } + return seasons + }, + [mediaRequest] + ); + + const available = useMemo( + () => { + const status = mediaRequest?.media?.[is4k ? 'status4k' : 'status']; + return status === MediaStatus.AVAILABLE + }, + [mediaRequest, is4k] + ); + return ( = ({ item, ...props }) => { releaseYear={releaseYear} canRequest={canRequest} posterSrc={imageSrc!!} + mediaType={mediaType} > - - + + = ({ item, ...props }) => { cachePolicy={"memory-disk"} contentFit="cover" style={{ - aspectRatio: "10/15", - width: "100%", + aspectRatio: ratio, + [horizontal ? 'height' : 'width']: "100%" }} onLoad={handleImageLoad} /> + {mediaRequest && showDownloadInfo && ( + <> + + {!available && !Number.isNaN(progress) && ( + <> + + + + {progress?.toFixed(0)}% + + + + )} + + {mediaRequest?.requestedBy.displayName} + + {requestedSeasons.length > 0 && ( + + )} + + )} - - {title} - {releaseYear} - + + + {title} + {releaseYear} ); diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index a1a1fb7c..0d77ac13 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -128,14 +128,12 @@ const RenderItem = ({ item, index }: any) => { const JellyseerrSeasons: React.FC<{ isLoading: boolean; - result?: TvResult; details?: TvDetails; hasAdvancedRequest?: boolean, onAdvancedRequest?: (data: MediaRequestBody) => void; refetch: (options?: (RefetchOptions | undefined)) => Promise>; }> = ({ isLoading, - result, details, refetch, hasAdvancedRequest, @@ -195,7 +193,7 @@ const JellyseerrSeasons: React.FC<{ return onAdvancedRequest?.(body) } - requestMedia(result?.name!!, body, refetch); + requestMedia(details.name, body, refetch); } }, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]); @@ -227,7 +225,7 @@ const JellyseerrSeasons: React.FC<{ return onAdvancedRequest?.(body) } - requestMedia(`${result?.name!!}, Season ${seasonNumber}`, body, refetch); + requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); } }, [requestMedia, hasAdvancedRequest, onAdvancedRequest]); diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 8525afd0..aad3b1d3 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -8,6 +8,7 @@ import { toast } from "sonner-native"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; import { useTranslation } from "react-i18next"; +import {Colors} from "@/constants/Colors"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); @@ -61,7 +62,7 @@ export const StorageSettings = () => { { ((size.total - size.remaining - size.app) / size.total) * 100 }%`, - backgroundColor: "rgb(192 132 252)", + backgroundColor: Colors.primaryLightRGB, }} /> diff --git a/constants/Colors.ts b/constants/Colors.ts index 69734810..8702c547 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -1,5 +1,7 @@ export const Colors = { primary: "#9334E9", + primaryRGB: "rgb(147 51 234)", + primaryLightRGB: "rgb(192 132 252)", text: "#ECEDEE", background: "#151718", tint: "#fff", diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 9d3b37c7..32e36513 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,5 +1,5 @@ import axios, { AxiosError, AxiosInstance } from "axios"; -import { Results } from "@/utils/jellyseerr/server/models/Search"; +import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; import { storage } from "@/utils/mmkv"; import { inRange } from "lodash"; import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; @@ -14,7 +14,7 @@ import { MediaType, } from "@/utils/jellyseerr/server/constants/media"; import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import { SeasonWithEpisodes, @@ -227,6 +227,23 @@ export class JellyseerrApi { .then(({ data }) => data); } + async getRequest(id: number): Promise { + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`) + .then(({ data }) => data); + } + + async requests(params = { + filter: "all", + take: 10, + sort: "modified", + skip: 0 + }): Promise { + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.REQUEST, {params}) + .then(({data}) => data); + } + async movieDetails(id: number) { return this.axios ?.get(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`) @@ -439,14 +456,34 @@ export const useJellyseerr = () => { ); const isJellyseerrResult = ( - items: any[] | null | undefined - ): items is Results[] => { + items: any | null | undefined + ): items is Results => { return ( - !items || - (items.length >= 0 && - Object.hasOwn(items[0], "mediaType") && - Object.values(MediaType).includes(items[0]["mediaType"])) - ); + items && + Object.hasOwn(items, "mediaType") && + Object.values(MediaType).includes(items["mediaType"]) + ) + }; + + const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { + return isJellyseerrResult(item) + ? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name) + : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) + }; + + const getYear = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { + return new Date(( + isJellyseerrResult(item) + ? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate) + : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate)) + || "" + )?.getFullYear?.() + }; + + const getMediaType = (item: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => { + return isJellyseerrResult(item) + ? item.mediaType + : item?.mediaInfo?.mediaType }; const jellyseerrRegion = useMemo( @@ -464,6 +501,9 @@ export const useJellyseerr = () => { setJellyseerrUser, clearAllJellyseerData, isJellyseerrResult, + getTitle, + getYear, + getMediaType, jellyseerrRegion, jellyseerrLocale, requestMedia, diff --git a/translations/de.json b/translations/de.json index 50995e15..962e10d7 100644 --- a/translations/de.json +++ b/translations/de.json @@ -168,7 +168,8 @@ "tv_quota_limit": "TV-Anfragelimit", "tv_quota_days": "TV-Anfragetage", "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", - "unlimited": "Unlimitiert" + "unlimited": "Unlimitiert", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Aktiviere Marlin Search", diff --git a/translations/en.json b/translations/en.json index ca19a136..5ba25ec9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -168,7 +168,8 @@ "tv_quota_limit": "TV quota limit", "tv_quota_days": "TV quota days", "reset_jellyseerr_config_button": "Reset Jellyseerr config", - "unlimited": "Unlimited" + "unlimited": "Unlimited", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Enable Marlin Search ", diff --git a/translations/es.json b/translations/es.json index c03c2e69..9004c2f8 100644 --- a/translations/es.json +++ b/translations/es.json @@ -168,7 +168,8 @@ "tv_quota_limit": "Límite de cuota de series", "tv_quota_days": "Días de cuota de series", "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", - "unlimited": "Ilimitado" + "unlimited": "Ilimitado", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Habilitar búsqueda de Marlin", diff --git a/translations/fr.json b/translations/fr.json index e9c576e6..7347d274 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -169,7 +169,8 @@ "tv_quota_limit": "Limite de quota TV", "tv_quota_days": "Jours de quota TV", "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", - "unlimited": "Illimité" + "unlimited": "Illimité", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Activer Marlin Search ", diff --git a/translations/it.json b/translations/it.json index d88ec71e..60a95bb1 100644 --- a/translations/it.json +++ b/translations/it.json @@ -168,7 +168,8 @@ "tv_quota_limit": "Limite di quota per le serie TV", "tv_quota_days": "Giorni di quota per le serie TV", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", - "unlimited": "Illimitato" + "unlimited": "Illimitato", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Abilita la ricerca Marlin ", diff --git a/translations/ja.json b/translations/ja.json index 616067d5..acd99ff2 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -167,7 +167,8 @@ "tv_quota_limit": "テレビのクオータ制限", "tv_quota_days": "テレビのクオータ日数", "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", - "unlimited": "無制限" + "unlimited": "無制限", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "マーリン検索を有効にする ", diff --git a/translations/nl.json b/translations/nl.json index 7912e6b4..d2129263 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -168,7 +168,8 @@ "tv_quota_limit": "Limiet serie quota", "tv_quota_days": "Serie Quota dagen", "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", - "unlimited": "Ongelimiteerd" + "unlimited": "Ongelimiteerd", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Marlin Search inschakelen ", diff --git a/translations/tr.json b/translations/tr.json index 7bc2e6d3..bdc140c8 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -167,7 +167,8 @@ "tv_quota_limit": "TV kota limiti", "tv_quota_days": "TV kota günleri", "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", - "unlimited": "Sınırsız" + "unlimited": "Sınırsız", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "Marlin Aramasını Etkinleştir ", diff --git a/translations/zh-CN.json b/translations/zh-CN.json index e3a834a6..94c1dad5 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -167,7 +167,8 @@ "tv_quota_limit": "剧集配额限制", "tv_quota_days": "剧集配额天数", "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", - "unlimited": "无限制" + "unlimited": "无限制", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "启用 Marlin 搜索", diff --git a/translations/zh-TW.json b/translations/zh-TW.json index f36174fd..2dbd287e 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -167,7 +167,8 @@ "tv_quota_limit": "電視配額限制", "tv_quota_days": "電視配額天數", "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", - "unlimited": "無限制" + "unlimited": "無限制", + "plus_n_more": "+{{n}} more" }, "marlin_search": { "enable_marlin_search": "啟用 Marlin 搜索", From 09e9462ac04e4adfcaa96e0bd2cae5394c7a8a28 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:10:15 -0500 Subject: [PATCH 40/93] feat: (iOS) Switch Video Players --- .gitattributes | 1 + .gitignore | 1 + app/(auth)/player/direct-player.tsx | 4 +- components/settings/OtherSettings.tsx | 33 +- components/video-player/controls/Controls.tsx | 89 ++-- .../controls/contexts/ControlContext.tsx | 1 - .../controls/contexts/VideoContext.tsx | 2 +- components/vlc/VideoDebugInfo.tsx | 2 +- .../{vlc-player/src => }/VlcPlayer.types.ts | 0 .../{vlc-player/src => }/VlcPlayerView.tsx | 18 +- modules/index.ts | 27 ++ modules/vlc-player-3/expo-module.config.json | 6 + modules/vlc-player-3/ios/VlcPlayer3.podspec | 23 ++ .../vlc-player-3/ios/VlcPlayer3Module.swift | 71 ++++ modules/vlc-player-3/ios/VlcPlayer3View.swift | 388 ++++++++++++++++++ modules/vlc-player-3/src/VlcPlayer3Module.ts | 5 + .../.gradle/8.9/checksums/checksums.lock | Bin 17 -> 0 bytes .../8.9/dependencies-accessors/gc.properties | 0 .../.gradle/8.9/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/8.9/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../android/.gradle/8.9/gc.properties | 0 .../buildOutputCleanup.lock | Bin 17 -> 0 bytes .../buildOutputCleanup/cache.properties | 2 - .../android/.gradle/vcs-1/gc.properties | 0 modules/vlc-player/index.ts | 68 --- modules/vlc-player/ios/VlcPlayer.podspec | 3 +- modules/vlc-player/ios/VlcPlayerView.swift | 1 - translations/de.json | 7 +- translations/en.json | 7 +- translations/es.json | 7 +- translations/fr.json | 7 +- translations/it.json | 7 +- translations/ja.json | 7 +- translations/nl.json | 7 +- translations/tr.json | 7 +- translations/zh-CN.json | 7 +- translations/zh-TW.json | 7 +- utils/atoms/settings.ts | 9 + 38 files changed, 676 insertions(+), 148 deletions(-) create mode 100644 .gitattributes rename modules/{vlc-player/src => }/VlcPlayer.types.ts (100%) rename modules/{vlc-player/src => }/VlcPlayerView.tsx (86%) create mode 100644 modules/index.ts create mode 100644 modules/vlc-player-3/expo-module.config.json create mode 100644 modules/vlc-player-3/ios/VlcPlayer3.podspec create mode 100644 modules/vlc-player-3/ios/VlcPlayer3Module.swift create mode 100644 modules/vlc-player-3/ios/VlcPlayer3View.swift create mode 100644 modules/vlc-player-3/src/VlcPlayer3Module.ts delete mode 100644 modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock delete mode 100644 modules/vlc-player/android/.gradle/8.9/dependencies-accessors/gc.properties delete mode 100644 modules/vlc-player/android/.gradle/8.9/fileChanges/last-build.bin delete mode 100644 modules/vlc-player/android/.gradle/8.9/fileHashes/fileHashes.lock delete mode 100644 modules/vlc-player/android/.gradle/8.9/gc.properties delete mode 100644 modules/vlc-player/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 modules/vlc-player/android/.gradle/buildOutputCleanup/cache.properties delete mode 100644 modules/vlc-player/android/.gradle/vcs-1/gc.properties delete mode 100644 modules/vlc-player/index.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..56dea966 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 2a0ce8db..00eb8098 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.* *.orig.* web-build/ modules/vlc-player/android/build +modules/vlc-player/android/.gradle bun.lockb # macOS diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 9a407ade..83bda8ce 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -5,13 +5,13 @@ import { Controls } from "@/components/video-player/controls/Controls"; import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; import { useWebSocket } from "@/hooks/useWebsockets"; -import { VlcPlayerView } from "@/modules/vlc-player"; +import { VlcPlayerView } from "@/modules"; import { PipStartedPayload, PlaybackStatePayload, ProgressUpdatePayload, VlcPlayerViewRef, -} from "@/modules/vlc-player/src/VlcPlayer.types"; +} from "@/modules/VlcPlayer.types"; const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index e14c00cd..b947b9a6 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; +import {ScreenOrientationEnum, useSettings, VideoPlayer} from "@/utils/atoms/settings"; import { BitrateSelector, BITRATES } from "@/components/BitrateSelector"; import { BACKGROUND_FETCH_TASK, @@ -22,6 +22,7 @@ import { ListItem } from "../list/ListItem"; import { useTranslation } from "react-i18next"; import DisabledSetting from "@/components/settings/DisabledSetting"; import Dropdown from "@/components/common/Dropdown"; +import {isNumber} from "lodash"; export const OtherSettings: React.FC = () => { const router = useRouter(); @@ -142,6 +143,36 @@ export const OtherSettings: React.FC = () => { /> + {(Platform.OS === "ios" || Platform.isTVOS)&& ( + + t(`home.settings.other.video_players.${VideoPlayer[item]}`)} + title={ + + + {t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)} + + + + } + label={t("home.settings.other.orientation")} + onSelected={(defaultPlayer) => + updateSettings({ defaultPlayer }) + } + /> + + )} + = ({ )} - {!Platform.isTV && ( + {!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && ( ) => void; } -const NativeViewManager = requireNativeViewManager("VlcPlayer"); +const VLCViewManager = requireNativeViewManager("VlcPlayer"); +const VLC3ViewManager = requireNativeViewManager("VlcPlayer3"); // Create a forwarded ref version of the native view const NativeView = React.forwardRef( - (props, ref) => + (props, ref) => { + const [settings] = useSettings(); + + if (Platform.OS === "ios" || Platform.isTVOS) { + if (settings.defaultPlayer == VideoPlayer.VLC_3) { + console.log("[Apple] Using Vlc Player 3") + return + } + } + console.log("Using default Vlc Player") + return + } ); const VlcPlayerView = React.forwardRef( diff --git a/modules/index.ts b/modules/index.ts new file mode 100644 index 00000000..397fc0eb --- /dev/null +++ b/modules/index.ts @@ -0,0 +1,27 @@ +import VlcPlayerView from "./VlcPlayerView"; +import { + PlaybackStatePayload, + ProgressUpdatePayload, + VideoLoadStartPayload, + VideoStateChangePayload, + VideoProgressPayload, + VlcPlayerSource, + TrackInfo, + ChapterInfo, + VlcPlayerViewProps, + VlcPlayerViewRef, +} from "./VlcPlayer.types"; + +export { + VlcPlayerView, + VlcPlayerViewProps, + VlcPlayerViewRef, + PlaybackStatePayload, + ProgressUpdatePayload, + VideoLoadStartPayload, + VideoStateChangePayload, + VideoProgressPayload, + VlcPlayerSource, + TrackInfo, + ChapterInfo, +}; diff --git a/modules/vlc-player-3/expo-module.config.json b/modules/vlc-player-3/expo-module.config.json new file mode 100644 index 00000000..1e6766d7 --- /dev/null +++ b/modules/vlc-player-3/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios", "tvos"], + "ios": { + "modules": ["VlcPlayer3Module"] + } +} diff --git a/modules/vlc-player-3/ios/VlcPlayer3.podspec b/modules/vlc-player-3/ios/VlcPlayer3.podspec new file mode 100644 index 00000000..15274a12 --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'VlcPlayer3' + s.version = '3.6.1b1' + s.summary = 'A sample project summary' + s.description = 'A sample project description' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.ios.dependency 'MobileVLCKit', s.version + s.tvos.dependency 'TVVLCKit', s.version + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/vlc-player-3/ios/VlcPlayer3Module.swift b/modules/vlc-player-3/ios/VlcPlayer3Module.swift new file mode 100644 index 00000000..c0e32606 --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3Module.swift @@ -0,0 +1,71 @@ +import ExpoModulesCore + +public class VlcPlayer3Module: Module { + public func definition() -> ModuleDefinition { + Name("VlcPlayer3") + View(VlcPlayer3View.self) { + Prop("source") { (view: VlcPlayer3View, source: [String: Any]) in + view.setSource(source) + } + + Prop("paused") { (view: VlcPlayer3View, paused: Bool) in + if paused { + view.pause() + } else { + view.play() + } + } + + Events( + "onPlaybackStateChanged", + "onVideoStateChange", + "onVideoLoadStart", + "onVideoLoadEnd", + "onVideoProgress", + "onVideoError", + "onPipStarted" + ) + + AsyncFunction("startPictureInPicture") { (view: VlcPlayer3View) in + view.startPictureInPicture() + } + + AsyncFunction("play") { (view: VlcPlayer3View) in + view.play() + } + + AsyncFunction("pause") { (view: VlcPlayer3View) in + view.pause() + } + + AsyncFunction("stop") { (view: VlcPlayer3View) in + view.stop() + } + + AsyncFunction("seekTo") { (view: VlcPlayer3View, time: Int32) in + view.seekTo(time) + } + + AsyncFunction("setAudioTrack") { (view: VlcPlayer3View, trackIndex: Int) in + view.setAudioTrack(trackIndex) + } + + AsyncFunction("getAudioTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in + return view.getAudioTracks() + } + + AsyncFunction("setSubtitleTrack") { (view: VlcPlayer3View, trackIndex: Int) in + view.setSubtitleTrack(trackIndex) + } + + AsyncFunction("getSubtitleTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in + return view.getSubtitleTracks() + } + + AsyncFunction("setSubtitleURL") { + (view: VlcPlayer3View, url: String, name: String) in + view.setSubtitleURL(url, name: name) + } + } + } +} diff --git a/modules/vlc-player-3/ios/VlcPlayer3View.swift b/modules/vlc-player-3/ios/VlcPlayer3View.swift new file mode 100644 index 00000000..b9189f9f --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3View.swift @@ -0,0 +1,388 @@ +import ExpoModulesCore +#if os(tvOS) +import TVVLCKit +#else +import MobileVLCKit +#endif +import UIKit + +class VlcPlayer3View: ExpoView { + private var mediaPlayer: VLCMediaPlayer? + private var videoView: UIView? + private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second + private var isPaused: Bool = false + private var currentGeometryCString: [CChar]? + private var lastReportedState: VLCMediaPlayerState? + private var lastReportedIsPlaying: Bool? + private var customSubtitles: [(internalName: String, originalName: String)] = [] + private var startPosition: Int32 = 0 + private var isMediaReady: Bool = false + private var externalTrack: [String: String]? + private var progressTimer: DispatchSourceTimer? + private var isStopping: Bool = false // Define isStopping here + private var lastProgressCall = Date().timeIntervalSince1970 + var hasSource = false + + // MARK: - Initialization + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + setupView() + setupNotifications() + } + + // MARK: - Setup + + private func setupView() { + DispatchQueue.main.async { + self.backgroundColor = .black + self.videoView = UIView() + self.videoView?.translatesAutoresizingMaskIntoConstraints = false + + if let videoView = self.videoView { + self.addSubview(videoView) + NSLayoutConstraint.activate([ + videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + videoView.topAnchor.constraint(equalTo: self.topAnchor), + videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + } + } + } + + private func setupNotifications() { + NotificationCenter.default.addObserver( + self, selector: #selector(applicationWillResignActive), + name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver( + self, selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, object: nil) + } + + // MARK: - Public Methods + func startPictureInPicture() { } + + @objc func play() { + self.mediaPlayer?.play() + self.isPaused = false + print("Play") + } + + @objc func pause() { + self.mediaPlayer?.pause() + self.isPaused = true + } + + @objc func seekTo(_ time: Int32) { + guard let player = self.mediaPlayer else { return } + + let wasPlaying = player.isPlaying + if wasPlaying { + self.pause() + } + + if let duration = player.media?.length.intValue { + print("Seeking to time: \(time) Video Duration \(duration)") + + // If the specified time is greater than the duration, seek to the end + let seekTime = time > duration ? duration - 1000 : time + player.time = VLCTime(int: seekTime) + + if wasPlaying { + self.play() + } + self.updatePlayerState() + } else { + print("Error: Unable to retrieve video duration") + } + } + + @objc func setSource(_ source: [String: Any]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if self.hasSource { + return + } + + let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:] + self.externalTrack = source["externalTrack"] as? [String: String] + var initOptions = source["initOptions"] as? [Any] ?? [] + self.startPosition = source["startPosition"] as? Int32 ?? 0 + initOptions.append("--start-time=\(self.startPosition)") + + guard let uri = source["uri"] as? String, !uri.isEmpty else { + print("Error: Invalid or empty URI") + self.onVideoError?(["error": "Invalid or empty URI"]) + return + } + + let autoplay = source["autoplay"] as? Bool ?? false + let isNetwork = source["isNetwork"] as? Bool ?? false + + self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()]) + self.mediaPlayer = VLCMediaPlayer(options: initOptions) + self.mediaPlayer?.delegate = self + self.mediaPlayer?.drawable = self.videoView + self.mediaPlayer?.scaleFactor = 0 + + let media: VLCMedia + if isNetwork { + print("Loading network file: \(uri)") + media = VLCMedia(url: URL(string: uri)!) + } else { + print("Loading local file: \(uri)") + if uri.starts(with: "file://"), let url = URL(string: uri) { + media = VLCMedia(url: url) + } else { + media = VLCMedia(path: uri) + } + } + + print("Debug: Media options: \(mediaOptions)") + media.addOptions(mediaOptions) + + self.mediaPlayer?.media = media + self.hasSource = true + + if autoplay { + print("Playing...") + self.play() + } + } + } + + @objc func setAudioTrack(_ trackIndex: Int) { + self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex) + } + + @objc func getAudioTracks() -> [[String: Any]]? { + guard let trackNames = mediaPlayer?.audioTrackNames, + let trackIndexes = mediaPlayer?.audioTrackIndexes + else { + return nil + } + + return zip(trackNames, trackIndexes).map { name, index in + return ["name": name, "index": index] + } + } + + @objc func setSubtitleTrack(_ trackIndex: Int) { + print("Debug: Attempting to set subtitle track to index: \(trackIndex)") + self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex) + print( + "Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)" + ) + } + + @objc func setSubtitleURL(_ subtitleURL: String, name: String) { + guard let url = URL(string: subtitleURL) else { + print("Error: Invalid subtitle URL") + return + } + + let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true) + if let result = result { + let internalName = "Track \(self.customSubtitles.count + 1)" + print("Subtitle added with result: \(result) \(internalName)") + self.customSubtitles.append((internalName: internalName, originalName: name)) + } else { + print("Failed to add subtitle") + } + } + + @objc func getSubtitleTracks() -> [[String: Any]]? { + guard let mediaPlayer = self.mediaPlayer else { + return nil + } + + let count = mediaPlayer.numberOfSubtitlesTracks + print("Debug: Number of subtitle tracks: \(count)") + + guard count > 0 else { + return nil + } + + var tracks: [[String: Any]] = [] + + if let names = mediaPlayer.videoSubTitlesNames as? [String], + let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] + { + for (index, name) in zip(indexes, names) { + if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) { + tracks.append(["name": customSubtitle.originalName, "index": index.intValue]) + } else { + tracks.append(["name": name, "index": index.intValue]) + } + } + } + + print("Debug: Subtitle tracks: \(tracks)") + return tracks + } + + @objc func stop(completion: (() -> Void)? = nil) { + guard !isStopping else { + completion?() + return + } + isStopping = true + + // If we're not on the main thread, dispatch to main thread + if !Thread.isMainThread { + DispatchQueue.main.async { [weak self] in + self?.performStop(completion: completion) + } + } else { + performStop(completion: completion) + } + } + + // MARK: - Private Methods + + @objc private func applicationWillResignActive() { + + } + + @objc private func applicationDidBecomeActive() { + + } + + private func performStop(completion: (() -> Void)? = nil) { + // Stop the media player + mediaPlayer?.stop() + + // Remove observer + NotificationCenter.default.removeObserver(self) + + // Clear the video view + videoView?.removeFromSuperview() + videoView = nil + + // Release the media player + mediaPlayer?.delegate = nil + mediaPlayer = nil + + isStopping = false + completion?() + } + + private func updateVideoProgress() { + guard let player = self.mediaPlayer else { return } + + let currentTimeMs = player.time.intValue + let durationMs = player.media?.length.intValue ?? 0 + + print("Debug: Current time: \(currentTimeMs)") + if currentTimeMs >= 0 && currentTimeMs < durationMs { + if player.isPlaying && !self.isMediaReady { + self.isMediaReady = true + // Set external track subtitle when starting. + if let externalTrack = self.externalTrack { + if let name = externalTrack["name"], !name.isEmpty { + let deliveryUrl = externalTrack["DeliveryUrl"] ?? "" + self.setSubtitleURL(deliveryUrl, name: name) + } + } + } + self.onVideoProgress?([ + "currentTime": currentTimeMs, + "duration": durationMs, + ]) + } + } + + // MARK: - Expo Events + + @objc var onPlaybackStateChanged: RCTDirectEventBlock? + @objc var onVideoLoadStart: RCTDirectEventBlock? + @objc var onVideoStateChange: RCTDirectEventBlock? + @objc var onVideoProgress: RCTDirectEventBlock? + @objc var onVideoLoadEnd: RCTDirectEventBlock? + @objc var onVideoError: RCTDirectEventBlock? + @objc var onPipStarted: RCTDirectEventBlock? + + // MARK: - Deinitialization + + deinit { + performStop() + } +} + +extension VlcPlayer3View: VLCMediaPlayerDelegate { + func mediaPlayerTimeChanged(_ aNotification: Notification) { + // self?.updateVideoProgress() + let timeNow = Date().timeIntervalSince1970 + if timeNow - lastProgressCall >= 1 { + lastProgressCall = timeNow + updateVideoProgress() + } + } + + func mediaPlayerStateChanged(_ aNotification: Notification) { + self.updatePlayerState() + } + + private func updatePlayerState() { + guard let player = self.mediaPlayer else { return } + let currentState = player.state + + var stateInfo: [String: Any] = [ + "target": self.reactTag ?? NSNull(), + "currentTime": player.time.intValue, + "duration": player.media?.length.intValue ?? 0, + "error": false, + ] + + if player.isPlaying { + stateInfo["isPlaying"] = true + stateInfo["isBuffering"] = false + stateInfo["state"] = "Playing" + } else { + stateInfo["isPlaying"] = false + stateInfo["state"] = "Paused" + } + + if player.state == VLCMediaPlayerState.buffering { + stateInfo["isBuffering"] = true + stateInfo["state"] = "Buffering" + } else if player.state == VLCMediaPlayerState.error { + print("player.state ~ error") + stateInfo["state"] = "Error" + self.onVideoLoadEnd?(stateInfo) + } else if player.state == VLCMediaPlayerState.opening { + print("player.state ~ opening") + stateInfo["state"] = "Opening" + } + + if self.lastReportedState != currentState + || self.lastReportedIsPlaying != player.isPlaying + { + self.lastReportedState = currentState + self.lastReportedIsPlaying = player.isPlaying + self.onVideoStateChange?(stateInfo) + } + + } +} + +extension VlcPlayer3View: VLCMediaDelegate { + // Implement VLCMediaDelegate methods if needed +} + +extension VLCMediaPlayerState { + var description: String { + switch self { + case .opening: return "Opening" + case .buffering: return "Buffering" + case .playing: return "Playing" + case .paused: return "Paused" + case .stopped: return "Stopped" + case .ended: return "Ended" + case .error: return "Error" + case .esAdded: return "ESAdded" + @unknown default: return "Unknown" + } + } +} diff --git a/modules/vlc-player-3/src/VlcPlayer3Module.ts b/modules/vlc-player-3/src/VlcPlayer3Module.ts new file mode 100644 index 00000000..b292aaff --- /dev/null +++ b/modules/vlc-player-3/src/VlcPlayer3Module.ts @@ -0,0 +1,5 @@ +import { requireNativeModule } from 'expo-modules-core'; + +// It loads the native module object from the JSI or falls back to +// the bridge module (from NativeModulesProxy) if the remote debugger is on. +export default requireNativeModule('VlcPlayer3'); diff --git a/modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock b/modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock deleted file mode 100644 index 52a7f0f4db1362d9d70601745a03ec2299581cfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 TcmZQxc(L*Ap=GabGC% void -): EventSubscription { - return emitter.addListener( - "onPlaybackStateChanged", - listener - ); -} - -export function addVideoLoadStartListener( - listener: (event: VideoLoadStartPayload) => void -): EventSubscription { - return emitter.addListener( - "onVideoLoadStart", - listener - ); -} - -export function addVideoStateChangeListener( - listener: (event: VideoStateChangePayload) => void -): EventSubscription { - return emitter.addListener( - "onVideoStateChange", - listener - ); -} - -export function addVideoProgressListener( - listener: (event: VideoProgressPayload) => void -): EventSubscription { - return emitter.addListener("onVideoProgress", listener); -} - -export { - VlcPlayerView, - VlcPlayerViewProps, - VlcPlayerViewRef, - PlaybackStatePayload, - ProgressUpdatePayload, - VideoLoadStartPayload, - VideoStateChangePayload, - VideoProgressPayload, - VlcPlayerSource, - TrackInfo, - ChapterInfo, -}; diff --git a/modules/vlc-player/ios/VlcPlayer.podspec b/modules/vlc-player/ios/VlcPlayer.podspec index 89e84814..46dbafd1 100644 --- a/modules/vlc-player/ios/VlcPlayer.podspec +++ b/modules/vlc-player/ios/VlcPlayer.podspec @@ -19,6 +19,5 @@ Pod::Spec.new do |s| 'DEFINES_MODULE' => 'YES', 'SWIFT_COMPILATION_MODE' => 'wholemodule' } - - s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" + s.source_files = "*.{h,m,mm,swift,hpp,cpp}" end diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index c40b5108..f02478d2 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -3,7 +3,6 @@ import UIKit import VLCKit import os - public class VLCPlayerView: UIView { func setupView(parent: UIView) { self.backgroundColor = .black diff --git a/translations/de.json b/translations/de.json index 962e10d7..cca7b183 100644 --- a/translations/de.json +++ b/translations/de.json @@ -128,7 +128,12 @@ "OTHER": "Andere", "UNKNOWN": "Unbekannt" }, - "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", "hide_libraries": "Bibliotheken ausblenden", "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", diff --git a/translations/en.json b/translations/en.json index 5ba25ec9..40594300 100644 --- a/translations/en.json +++ b/translations/en.json @@ -129,11 +129,16 @@ "UNKNOWN": "Unknown" }, "safe_area_in_controls": "Safe area in controls", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Show Custom Menu Links", "hide_libraries": "Hide Libraries", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality" + "default_quality": "Default quality", }, "downloads": { "downloads_title": "Downloads", diff --git a/translations/es.json b/translations/es.json index 9004c2f8..9a2962a3 100644 --- a/translations/es.json +++ b/translations/es.json @@ -128,7 +128,12 @@ "OTHER": "Otra", "UNKNOWN": "Desconocida" }, - "safe_area_in_controls": "Área segura en controles", + "safe_area_in_controls": "Área segura en controles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Mostrar enlaces de menú personalizados", "hide_libraries": "Ocultar bibliotecas", "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", diff --git a/translations/fr.json b/translations/fr.json index 7347d274..719b5fae 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -128,7 +128,12 @@ "OTHER": "Autre", "UNKNOWN": "Inconnu" }, - "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Afficher les liens personnalisés", "hide_libraries": "Cacher des bibliothèques", "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.", diff --git a/translations/it.json b/translations/it.json index 60a95bb1..fc713f8e 100644 --- a/translations/it.json +++ b/translations/it.json @@ -128,7 +128,12 @@ "OTHER": "Altro", "UNKNOWN": "Sconosciuto" }, - "safe_area_in_controls": "Area sicura per i controlli", + "safe_area_in_controls": "Area sicura per i controlli", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Mostra i link del menu personalizzato", "hide_libraries": "Nascondi Librerie", "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", diff --git a/translations/ja.json b/translations/ja.json index acd99ff2..085b6c3d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -128,7 +128,12 @@ "OTHER": "その他", "UNKNOWN": "不明" }, - "safe_area_in_controls": "コントロールの安全エリア", + "safe_area_in_controls": "コントロールの安全エリア", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "カスタムメニューのリンクを表示", "hide_libraries": "ライブラリを非表示", "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", diff --git a/translations/nl.json b/translations/nl.json index d2129263..0e44a305 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -128,7 +128,12 @@ "OTHER": "Andere", "UNKNOWN": "Onbekend" }, - "safe_area_in_controls": "Veilig gebied in bedieningen", + "safe_area_in_controls": "Veilig gebied in bedieningen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Aangepaste menulinks tonen", "hide_libraries": "Verberg Bibliotheken", "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", diff --git a/translations/tr.json b/translations/tr.json index bdc140c8..a9c65b02 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -128,7 +128,12 @@ "OTHER": "Diğer", "UNKNOWN": "Bilinmeyen" }, - "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", "hide_libraries": "Kütüphaneleri Gizle", "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 94c1dad5..2fb3abf9 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -128,7 +128,12 @@ "OTHER": "其他", "UNKNOWN": "未知" }, - "safe_area_in_controls": "控制中的安全区域", + "safe_area_in_controls": "控制中的安全区域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "显示自定义菜单链接", "hide_libraries": "隐藏媒体库", "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 2dbd287e..3f127a6b 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -128,7 +128,12 @@ "OTHER": "其他", "UNKNOWN": "未知" }, - "safe_area_in_controls": "控制中的安全區域", + "safe_area_in_controls": "控制中的安全區域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, "show_custom_menu_links": "顯示自定義菜單鏈接", "hide_libraries": "隱藏媒體庫", "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 2426be4f..a1ba7d0b 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -14,6 +14,7 @@ import { import { Bitrate, BITRATES } from "@/components/BitrateSelector"; import { apiAtom } from "@/providers/JellyfinProvider"; import { writeInfoLog } from "@/utils/log"; +import {Video} from "@/utils/jellyseerr/server/models/Movie"; const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; @@ -112,6 +113,12 @@ export type HomeSectionNextUpResolver = { enableRewatching?: boolean; }; +export enum VideoPlayer { + // NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted + VLC_3, + VLC_4 +} + export type Settings = { home?: Home | null; autoRotate?: boolean; @@ -146,6 +153,7 @@ export type Settings = { jellyseerrServerUrl?: string; hiddenLibraries?: string[]; enableH265ForChromecast: boolean; + defaultPlayer: VideoPlayer; }; export interface Lockable { @@ -200,6 +208,7 @@ const defaultValues: Settings = { jellyseerrServerUrl: undefined, hiddenLibraries: [], enableH265ForChromecast: false, + defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android }; const loadSettings = (): Partial => { From e1314077e29659afbd12bea0e2199d19b6c7911c Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 3 Mar 2025 01:04:49 -0500 Subject: [PATCH 41/93] fix: only show recent requests when request is done loading --- components/jellyseerr/discover/RecentRequestsSlide.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index 5ab234d8..fc9a5064 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -34,7 +34,7 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { }); return ( - details && + !isLoading && details && ) } From 7f0726017776c27f8050a008446c55781f27432a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 15:18:09 +0100 Subject: [PATCH 42/93] fix: hide setting (use only vlc3 for now) --- components/settings/OtherSettings.tsx | 70 +++++++-------------------- 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index b947b9a6..7dbaed33 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -import {ScreenOrientationEnum, useSettings, VideoPlayer} from "@/utils/atoms/settings"; +import { ScreenOrientationEnum, useSettings, VideoPlayer } from "@/utils/atoms/settings"; import { BitrateSelector, BITRATES } from "@/components/BitrateSelector"; import { BACKGROUND_FETCH_TASK, @@ -7,9 +7,7 @@ import { unregisterBackgroundFetchAsync, } from "@/utils/background-tasks"; import { Ionicons } from "@expo/vector-icons"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; +const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; import { useRouter } from "expo-router"; @@ -22,7 +20,7 @@ import { ListItem } from "../list/ListItem"; import { useTranslation } from "react-i18next"; import DisabledSetting from "@/components/settings/DisabledSetting"; import Dropdown from "@/components/common/Dropdown"; -import {isNumber} from "lodash"; +import { isNumber } from "lodash"; export const OtherSettings: React.FC = () => { const router = useRouter(); @@ -85,10 +83,7 @@ export const OtherSettings: React.FC = () => { return ( - + { ScreenOrientationEnum[item]} title={ @@ -116,17 +105,11 @@ export const OtherSettings: React.FC = () => { {t(ScreenOrientationEnum[settings.defaultVideoOrientation])} - + } label={t("home.settings.other.orientation")} - onSelected={(defaultVideoOrientation) => - updateSettings({ defaultVideoOrientation }) - } + onSelected={(defaultVideoOrientation) => updateSettings({ defaultVideoOrientation })} /> @@ -137,13 +120,11 @@ export const OtherSettings: React.FC = () => { - updateSettings({ safeAreaInControlsEnabled: value }) - } + onValueChange={(value) => updateSettings({ safeAreaInControlsEnabled: value })} /> - {(Platform.OS === "ios" || Platform.isTVOS)&& ( + {/* {(Platform.OS === "ios" || Platform.isTVOS)&& ( { } /> - )} + )} */} - Linking.openURL( - "https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links" - ) - } + onPress={() => Linking.openURL("https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links")} > - updateSettings({ showCustomMenuLinks: value }) - } + onValueChange={(value) => updateSettings({ showCustomMenuLinks: value })} /> { title={t("home.settings.other.hide_libraries")} showArrow /> - + { selected={settings.defaultBitrate} title={ - - {settings.defaultBitrate?.key} - - + {settings.defaultBitrate?.key} + } label={t("home.settings.other.default_quality")} @@ -228,9 +194,7 @@ export const OtherSettings: React.FC = () => { - updateSettings({ disableHapticFeedback }) - } + onValueChange={(disableHapticFeedback) => updateSettings({ disableHapticFeedback })} /> From ec914133d6ae1696a4dd08c4e3fd9ee71d0f29db Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 15:19:59 +0100 Subject: [PATCH 43/93] fix: undefined var --- app/(auth)/player/direct-player.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 83bda8ce..03c6f618 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -3,6 +3,7 @@ import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; import { Controls } from "@/components/video-player/controls/Controls"; import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener"; +import { useHaptic } from "@/hooks/useHaptic"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; import { useWebSocket } from "@/hooks/useWebsockets"; import { VlcPlayerView } from "@/modules"; @@ -12,31 +13,29 @@ import { ProgressUpdatePayload, VlcPlayerViewRef, } from "@/modules/VlcPlayer.types"; -const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { writeToLog } from "@/utils/log"; import native from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; -import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; -import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; -import { useHaptic } from "@/hooks/useHaptic"; -import { useGlobalSearchParams, useNavigation } from "expo-router"; -import { useAtomValue } from "jotai"; -import React, { useCallback, useMemo, useRef, useState, useEffect } from "react"; -import { Alert, View, Platform } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; -import { useSettings } from "@/utils/atoms/settings"; -import { useTranslation } from "react-i18next"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BaseItemDto, MediaSourceInfo, PlaybackOrder, PlaybackProgressInfo, - PlaybackStartInfo, RepeatMode, } from "@jellyfin/sdk/lib/generated-client"; +import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; +import { useGlobalSearchParams, useNavigation } from "expo-router"; +import { useAtomValue } from "jotai"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, Platform, View } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null; export default function page() { const videoRef = useRef(null); @@ -219,6 +218,7 @@ export default function page() { }, [navigation, stop]); const currentPlayStateInfo = () => { + if (!stream) return; return { itemId: item?.Id!, audioStreamIndex: audioIndex ? audioIndex : undefined, From bce9ed26906015196848d9b5108be03cf5216657 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 15:20:13 +0100 Subject: [PATCH 44/93] fix: wrong prop --- components/settings/OtherSettings.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 7dbaed33..58086ada 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -176,7 +176,6 @@ export const OtherSettings: React.FC = () => { disabled={pluginSettings?.defaultBitrate?.locked} keyExtractor={(item) => item.key} titleExtractor={(item) => item.key} - selected={settings.defaultBitrate} title={ {settings.defaultBitrate?.key} From 9c9785ba9e78790aadf1d358fcd5db982651a587 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 15:20:23 +0100 Subject: [PATCH 45/93] chore --- components/settings/OtherSettings.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 58086ada..932ad967 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -1,26 +1,24 @@ -import { Platform } from "react-native"; -import { ScreenOrientationEnum, useSettings, VideoPlayer } from "@/utils/atoms/settings"; -import { BitrateSelector, BITRATES } from "@/components/BitrateSelector"; +import { BITRATES } from "@/components/BitrateSelector"; +import Dropdown from "@/components/common/Dropdown"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; import { BACKGROUND_FETCH_TASK, registerBackgroundFetchAsync, unregisterBackgroundFetchAsync, } from "@/utils/background-tasks"; import { Ionicons } from "@expo/vector-icons"; -const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; import { useRouter } from "expo-router"; import React, { useEffect, useMemo } from "react"; -import { Linking, Switch, TouchableOpacity } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Linking, Platform, Switch, TouchableOpacity } from "react-native"; import { toast } from "sonner-native"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; -import Dropdown from "@/components/common/Dropdown"; -import { isNumber } from "lodash"; +const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; +const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; export const OtherSettings: React.FC = () => { const router = useRouter(); From fe3b652b4fad5b2f117f27874630e637af47b6b8 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 15:21:18 +0100 Subject: [PATCH 46/93] fix: more specified dep arr --- app/(auth)/player/_layout.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index 0eefb300..c06c05a2 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -21,12 +21,10 @@ export default function Layout() { if (settings.autoRotate === true) { ScreenOrientation.unlockAsync(); } else { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }; - }, [settings]); + }, [settings.defaultVideoOrientation, settings.autoRotate]); return ( <> From 12ceef02cd9508f9fc363b82dc4703c35c2b6951 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Mon, 3 Mar 2025 16:01:27 +0100 Subject: [PATCH 47/93] fix: mark as played --- utils/jellyfin/playstate/markAsPlayed.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index 94c62cb7..467749f2 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -32,15 +32,6 @@ export const markAsPlayed = async ({ { userId, datePlayed: new Date().toISOString() }, { headers: getAuthHeaders(api) }, ), - api.axiosInstance.post( - `${api.basePath}/Sessions/Playing/Progress`, - { - ItemId: item.Id, - PositionTicks: item.RunTimeTicks, - MediaSourceId: item.Id, - }, - { headers: getAuthHeaders(api) }, - ), ]); return playedResponse.status === 200 && progressResponse.status === 200; From 77dba04289212ddaf1dbd82438f011b3069b4f2b Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 16:06:48 +0100 Subject: [PATCH 48/93] fix --- utils/jellyfin/playstate/markAsPlayed.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index 467749f2..d3a34725 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -15,26 +15,22 @@ interface MarkAsPlayedParams { * @param params - The parameters for marking an item as played * @returns A promise that resolves to true if the operation was successful, false otherwise */ -export const markAsPlayed = async ({ - api, - item, - userId, -}: MarkAsPlayedParams): Promise => { +export const markAsPlayed = async ({ api, item, userId }: MarkAsPlayedParams): Promise => { if (!api || !item?.Id || !userId || !item.RunTimeTicks) { console.error("Invalid parameters for markAsPlayed"); return false; } try { - const [playedResponse, progressResponse] = await Promise.all([ + const [playedResponse] = await Promise.all([ api.axiosInstance.post( `${api.basePath}/UserPlayedItems/${item.Id}`, { userId, datePlayed: new Date().toISOString() }, - { headers: getAuthHeaders(api) }, + { headers: getAuthHeaders(api) } ), ]); - return playedResponse.status === 200 && progressResponse.status === 200; + return playedResponse.status === 200; } catch (error) { return false; } From ebcb414b89bfd0fc3e6cd61dedb1840cacfa9c63 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 3 Mar 2025 16:10:47 +0100 Subject: [PATCH 49/93] fix: use sdk util --- utils/jellyfin/playstate/markAsPlayed.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index d3a34725..9b6ee13d 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -1,7 +1,6 @@ import { Api } from "@jellyfin/sdk"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { AxiosError } from "axios"; -import { getAuthHeaders } from "../jellyfin"; +import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; interface MarkAsPlayedParams { api: Api | null | undefined; @@ -12,7 +11,7 @@ interface MarkAsPlayedParams { /** * Marks a media item as played and updates its progress to completion. * - * @param params - The parameters for marking an item as played + * @param params - The parameters for marking an item as played∏ * @returns A promise that resolves to true if the operation was successful, false otherwise */ export const markAsPlayed = async ({ api, item, userId }: MarkAsPlayedParams): Promise => { @@ -22,15 +21,12 @@ export const markAsPlayed = async ({ api, item, userId }: MarkAsPlayedParams): P } try { - const [playedResponse] = await Promise.all([ - api.axiosInstance.post( - `${api.basePath}/UserPlayedItems/${item.Id}`, - { userId, datePlayed: new Date().toISOString() }, - { headers: getAuthHeaders(api) } - ), - ]); + const response = await getPlaystateApi(api).markPlayedItem({ + itemId: item.Id, + datePlayed: new Date().toISOString(), + }); - return playedResponse.status === 200; + return response.status === 200; } catch (error) { return false; } From 4dddc0f92640226dbbf931b5de8fbd10c7b95ac9 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:09:15 -0500 Subject: [PATCH 50/93] fix: Jellyseerr Recent Request slide fixes - added src for backdrop + poster - fixed horizontal height issues --- .../discover/RecentRequestsSlide.tsx | 6 ++--- components/posters/JellyseerrPoster.tsx | 23 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index fc9a5064..9a9902a6 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -34,7 +34,7 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { }); return ( - !isLoading && details && + details && ) } @@ -50,9 +50,7 @@ const RecentRequestsSlide: React.FC = ({ slide, ...props }); return ( - requests && - requests.results.length > 0 && - !isError && ( + requests && ( = ({ mediaRequest, ...props }) => { - const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr(); + const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr(); const loadingOpacity = useSharedValue(1); const imageOpacity = useSharedValue(0); const {t} = useTranslation(); - const loadingAnimatedStyle = useAnimatedStyle(() => ({ - opacity: loadingOpacity.value, - })); - const imageAnimatedStyle = useAnimatedStyle(() => ({ opacity: imageOpacity.value, })); @@ -51,11 +47,13 @@ const JellyseerrPoster: React.FC = ({ imageOpacity.value = withTiming(1, { duration: 300 }); }; - const imageSrc = useMemo( - () => jellyseerrApi?.imageProxy( - horizontal ? item.backdropPath : item.posterPath, - horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face" - ), + const backdropSrc = useMemo( + () => jellyseerrApi?.imageProxy(item.backdropPath, "w1920_and_h800_multi_faces"), + [item, jellyseerrApi, horizontal] + ); + + const posterSrc = useMemo( + () => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face",), [item, jellyseerrApi, horizontal] ); @@ -116,16 +114,17 @@ const JellyseerrPoster: React.FC = ({ mediaTitle={title} releaseYear={releaseYear} canRequest={canRequest} - posterSrc={imageSrc!!} + posterSrc={posterSrc!!} mediaType={mediaType} > Date: Wed, 5 Mar 2025 00:32:30 -0500 Subject: [PATCH 51/93] feat: Better Jellyseerr search results #586 - fetch 4 pages at once to maximize search results - add local sorting options --- .../jellyseerr/company/[companyId].tsx | 6 +- app/(auth)/(tabs)/(search)/index.tsx | 129 +++++++++++------- components/filters/FilterButton.tsx | 2 +- components/filters/FilterSheet.tsx | 4 +- components/jellyseerr/JellyseerrIndexPage.tsx | 113 ++++++++++----- components/search/SearchItemWrapper.tsx | 29 ++-- hooks/useJellyseerr.ts | 9 +- translations/de.json | 9 +- translations/en.json | 9 +- translations/es.json | 9 +- translations/fr.json | 9 +- translations/it.json | 9 +- translations/ja.json | 9 +- translations/nl.json | 9 +- translations/tr.json | 9 +- translations/zh-CN.json | 9 +- translations/zh-TW.json | 9 +- 17 files changed, 264 insertions(+), 118 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx index c5eda557..cf8111bb 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx @@ -1,12 +1,8 @@ -import {router, useLocalSearchParams, useSegments,} from "expo-router"; +import {useLocalSearchParams} from "expo-router"; import React, {useMemo,} from "react"; -import {TouchableOpacity} from "react-native"; import {useInfiniteQuery} from "@tanstack/react-query"; import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Text} from "@/components/common/Text"; import {Image} from "expo-image"; -import Poster from "@/components/posters/Poster"; -import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 6d1ac344..c7e29512 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,39 +1,30 @@ -import { Input } from "@/components/common/Input"; -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import {Text} from "@/components/common/Text"; +import {TouchableItemRouter} from "@/components/common/TouchableItemRouter"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { Tag } from "@/components/GenreTags"; -import { ItemCardText } from "@/components/ItemCardText"; -import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage"; +import {Tag} from "@/components/GenreTags"; +import {ItemCardText} from "@/components/ItemCardText"; +import {JellyseerrSearchSort, JellyserrIndexPage} from "@/components/jellyseerr/JellyseerrIndexPage"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; -import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; -import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { - BaseItemDto, - BaseItemKind, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; +import {LoadingSkeleton} from "@/components/search/LoadingSkeleton"; +import {SearchItemWrapper} from "@/components/search/SearchItemWrapper"; +import {useJellyseerr} from "@/hooks/useJellyseerr"; +import {apiAtom, userAtom} from "@/providers/JellyfinProvider"; +import {useSettings} from "@/utils/atoms/settings"; +import {BaseItemDto, BaseItemKind,} from "@jellyfin/sdk/lib/generated-client/models"; +import {getItemsApi, getSearchApi} from "@jellyfin/sdk/lib/utils/api"; +import {useQuery} from "@tanstack/react-query"; import axios from "axios"; -import { Href, router, useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import React, { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; -import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useDebounce } from "use-debounce"; -import { useTranslation } from "react-i18next"; -import { eventBus } from "@/utils/eventBus"; +import {router, useLocalSearchParams, useNavigation} from "expo-router"; +import {useAtom} from "jotai"; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,} from "react"; +import {Platform, ScrollView, TouchableOpacity, View} from "react-native"; +import {useSafeAreaInsets} from "react-native-safe-area-context"; +import {useDebounce} from "use-debounce"; +import {useTranslation} from "react-i18next"; +import {eventBus} from "@/utils/eventBus"; +import {sortOrderOptions} from "@/utils/atoms/filters"; +import {FilterButton} from "@/components/filters/FilterButton"; type SearchType = "Library" | "Discover"; @@ -64,6 +55,8 @@ export default function search() { const [settings] = useSettings(); const { jellyseerrApi } = useJellyseerr(); + const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort.DEFAULT) + const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc") const searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -241,26 +234,52 @@ export default function search() { }} > {jellyseerrApi && ( - - setSearchType("Library")}> - - - setSearchType("Discover")}> - - - + <> + + setSearchType("Library")}> + + + setSearchType("Discover")}> + + + {!loading && noResults && debouncedSearch.length > 0 && ( + + Object.keys(JellyseerrSearchSort).filter(v => isNaN(Number(v)))} + set={value => setJellyseerrOrderBy(value[0])} + values={[jellyseerrOrderBy]} + title={t("library.filters.sort_by")} + renderItemLabel={(item) => t(`home.settings.plugins.jellyseerr.order_by.${item}`)} + showSearch={false} + /> + ["asc", "desc"]} + set={value => setJellyseerrSortOrder(value[0])} + values={[jellyseerrSortOrder]} + title={t("library.filters.sort_order")} + renderItemLabel={(item) => t(`library.filters.${item}`)} + showSearch={false} + /> + + )} + + )} @@ -353,7 +372,11 @@ export default function search() { /> ) : ( - + )} {searchType === "Library" && ( diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index de8caa2e..a96e7348 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -13,7 +13,7 @@ interface FilterButtonProps extends ViewProps { title: string; set: (value: T[]) => void; queryFn: (params: any) => Promise; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; icon?: "filter" | "sort"; } diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index cc5d4300..7e8c3cd2 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -28,7 +28,7 @@ interface Props extends ViewProps { values: T[]; set: (value: T[]) => void; title: string; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; showSearch?: boolean; } @@ -88,7 +88,7 @@ export const FilterSheet = ({ if (!search) return _data; const results = []; for (let i = 0; i < (_data?.length || 0); i++) { - if (_data && searchFilter(_data[i], search)) { + if (_data && searchFilter?.(_data[i], search)) { results.push(_data[i]); } } diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 55b45b80..0363ae1e 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -7,7 +7,7 @@ import { TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; -import React, { useMemo } from "react"; +import React, {useMemo, useState} from "react"; import { View, ViewProps } from "react-native"; import { useAnimatedReaction, @@ -21,17 +21,32 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; import { useTranslation } from "react-i18next"; -import {uniqBy} from "lodash"; +import {orderBy, uniqBy} from "lodash"; +import {useInfiniteQuery} from "@tanstack/react-query"; interface Props extends ViewProps { searchQuery: string; + sortType?: JellyseerrSearchSort; + order?: "asc" | "desc"; } -export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { +export enum JellyseerrSearchSort { + DEFAULT, + VOTE_COUNT_AND_AVERAGE, + POPULARITY +} + +export const JellyserrIndexPage: React.FC = ({ + searchQuery, + sortType, + order +}) => { const { jellyseerrApi } = useJellyseerr(); const opacity = useSharedValue(1); const { t } = useTranslation(); + const [loadInitialPages, setLoadInitialPages] = useState(false) + const { data: jellyseerrDiscoverSettings, isFetching: f1, @@ -43,30 +58,33 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { }); const { - data: jellyseerrResults, + data: jellyseerrResultPages, isFetching: f2, isLoading: l2, - } = useReactNavigationQuery({ + isFetchingNextPage: n2, + hasNextPage, + fetchNextPage + } = useInfiniteQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], - queryFn: async () => { - const response = await jellyseerrApi?.search({ - query: new URLSearchParams(searchQuery).toString(), - page: 1, - language: "en", - }); - return response?.results; - }, + queryFn: async ({pageParam}) => + jellyseerrApi?.search({ + query: new URLSearchParams(searchQuery || "").toString(), + page: Number(pageParam), + }), enabled: !!jellyseerrApi && searchQuery.length > 0, - }); + staleTime: 0, + initialPageParam: 1, + getNextPageParam: (lastPage, pages) => { + const firstPage = pages?.[0] + const mostRecentPage = lastPage || pages?.[pages?.length - 1] + const currentPage = mostRecentPage?.page || 1 - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; + return Math.min(currentPage + 1, firstPage?.totalPages || 1) + }, }); useAnimatedReaction( - () => f1 || f2 || l1 || l2, + () => f1 || f2 || l1 || l2 || n2, (isLoading) => { if (isLoading) { opacity.value = withTiming(1, { duration: 200 }); @@ -76,31 +94,63 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { } ); + const sortingType = useMemo( + () => { + if (!sortType) return; + switch (Number(JellyseerrSearchSort[sortType])) { + case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: + return ["voteCount", "voteAverage"]; + case JellyseerrSearchSort.POPULARITY: + return ["voteCount", "popularity"] + default: + return undefined + } + }, + [sortType, order] + ) + + const jellyseerrResults = useMemo( + () => { + const lastPage = jellyseerrResultPages?.pages?.[jellyseerrResultPages?.pages?.length - 1] + + if ((lastPage?.page || 0) % 5 !== 0 && hasNextPage && !loadInitialPages) { + fetchNextPage() + setLoadInitialPages(lastPage?.page === 4 || (lastPage !== undefined && lastPage.totalPages == lastPage.page)) + } + + return uniqBy(jellyseerrResultPages?.pages?.flatMap?.(page => page?.results || []), "id") + }, + [jellyseerrResultPages, fetchNextPage, hasNextPage] + ); + const jellyseerrMovieResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[], - "id" + sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); const jellyseerrTvResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], - "id" + sortingType || [t => t.originalName.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); const jellyseerrPersonResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[], - "id" + sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); if (!searchQuery.length) @@ -112,7 +162,7 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { return ( - + {!jellyseerrMovieResults?.length && !jellyseerrTvResults?.length && @@ -120,7 +170,8 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { !f1 && !f2 && !l1 && - !l2 && ( + !l2 && + !loadInitialPages && ( {t("search.no_results_found_for")} @@ -131,7 +182,7 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { )} - + = { ids?: string[] | null; items?: T[]; renderItem: (item: any) => React.ReactNode; header?: string; + onEndReached?: (() => void) | null | undefined; }; export const SearchItemWrapper = ({ @@ -19,6 +20,7 @@ export const SearchItemWrapper = ({ items, renderItem, header, + onEndReached }: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -54,17 +56,22 @@ export const SearchItemWrapper = ({ return ( <> {header} - - {data && data?.length > 0 - ? data.map((item) => renderItem(item)) - : items && items?.length > 0 - ? items.map((i) => renderItem(i)) - : undefined} - + keyExtractor={(_, index) => index.toString()} + estimatedItemSize={250} + /*@ts-ignore */ + data={data || items} + onEndReachedThreshold={1} + onEndReached={onEndReached} + //@ts-ignore + renderItem={({item, index}) => item ? renderItem(item) : <>} + /> ); }; diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 32e36513..7c860180 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -44,7 +44,7 @@ import { interface SearchParams { query: string; page: number; - language: string; + // language: string; } interface SearchResults { @@ -214,11 +214,10 @@ export class JellyseerrApi { } async search(params: SearchParams): Promise { - const response = await this.axios?.get( + return this.axios?.get( Endpoints.API_V1 + Endpoints.SEARCH, { params } - ); - return response?.data; + ).then(({ data }) => data) } async request(request: MediaRequestBody): Promise { @@ -467,7 +466,7 @@ export const useJellyseerr = () => { const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { return isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name) + ? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name) : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) }; diff --git a/translations/de.json b/translations/de.json index cca7b183..19258147 100644 --- a/translations/de.json +++ b/translations/de.json @@ -174,7 +174,12 @@ "tv_quota_days": "TV-Anfragetage", "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", "unlimited": "Unlimitiert", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Aktiviere Marlin Search", @@ -329,6 +334,8 @@ "years": "Jahre", "sort_by": "Sortieren nach", "sort_order": "Sortierreihenfolge", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/en.json b/translations/en.json index 40594300..e804e5cb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -174,7 +174,12 @@ "tv_quota_days": "TV quota days", "reset_jellyseerr_config_button": "Reset Jellyseerr config", "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Enable Marlin Search ", @@ -333,6 +338,8 @@ "years": "Years", "sort_by": "Sort By", "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/es.json b/translations/es.json index 9a2962a3..463d86f2 100644 --- a/translations/es.json +++ b/translations/es.json @@ -174,7 +174,12 @@ "tv_quota_days": "Días de cuota de series", "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", "unlimited": "Ilimitado", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Habilitar búsqueda de Marlin", @@ -329,6 +334,8 @@ "years": "Años", "sort_by": "Ordenar por", "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiquetas" } }, diff --git a/translations/fr.json b/translations/fr.json index 719b5fae..6df653ff 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -175,7 +175,12 @@ "tv_quota_days": "Jours de quota TV", "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", "unlimited": "Illimité", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Activer Marlin Search ", @@ -330,6 +335,8 @@ "years": "Années", "sort_by": "Trier par", "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/it.json b/translations/it.json index fc713f8e..87882704 100644 --- a/translations/it.json +++ b/translations/it.json @@ -174,7 +174,12 @@ "tv_quota_days": "Giorni di quota per le serie TV", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", "unlimited": "Illimitato", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Abilita la ricerca Marlin ", @@ -329,6 +334,8 @@ "years": "Anni", "sort_by": "Ordina per", "sort_order": "Criterio di ordinamento", + "asc": "Ascending", + "desc": "Descending", "tags": "Tag" } }, diff --git a/translations/ja.json b/translations/ja.json index 085b6c3d..dfdbc59d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -173,7 +173,12 @@ "tv_quota_days": "テレビのクオータ日数", "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", "unlimited": "無制限", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "マーリン検索を有効にする ", @@ -328,6 +333,8 @@ "years": "年", "sort_by": "ソート", "sort_order": "ソート順", + "asc": "Ascending", + "desc": "Descending", "tags": "タグ" } }, diff --git a/translations/nl.json b/translations/nl.json index 0e44a305..1f87dfc8 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -174,7 +174,12 @@ "tv_quota_days": "Serie Quota dagen", "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", "unlimited": "Ongelimiteerd", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Marlin Search inschakelen ", @@ -329,6 +334,8 @@ "years": "Jaren", "sort_by": "Sorteren op", "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", "tags": "Labels" } }, diff --git a/translations/tr.json b/translations/tr.json index a9c65b02..47f6fc02 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -173,7 +173,12 @@ "tv_quota_days": "TV kota günleri", "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", "unlimited": "Sınırsız", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Marlin Aramasını Etkinleştir ", @@ -328,6 +333,8 @@ "years": "Yıllar", "sort_by": "Sırala", "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiketler" } }, diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 2fb3abf9..ad2d0468 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -173,7 +173,12 @@ "tv_quota_days": "剧集配额天数", "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", "unlimited": "无限制", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "启用 Marlin 搜索", @@ -328,6 +333,8 @@ "years": "年份", "sort_by": "排序依据", "sort_order": "排序顺序", + "asc": "Ascending", + "desc": "Descending", "tags": "标签" } }, diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 3f127a6b..d4f42fe3 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -173,7 +173,12 @@ "tv_quota_days": "電視配額天數", "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", "unlimited": "無限制", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "啟用 Marlin 搜索", @@ -328,6 +333,8 @@ "years": "年份", "sort_by": "排序依據", "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", "tags": "標籤" } }, From 79a2873975dd4a1cdc558c88ac941a75501ea847 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 00:35:54 -0500 Subject: [PATCH 52/93] fix: Fix search item count translation --- components/filters/FilterSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index 7e8c3cd2..7b14af03 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -155,7 +155,7 @@ export const FilterSheet = ({ > {title} - {t("search.items", {count: _data?.length})} + {t("search.x_items", {count: _data?.length})} {showSearch && ( Date: Wed, 5 Mar 2025 00:47:47 -0500 Subject: [PATCH 53/93] fix: Jellyseerr order by "default" not preselected (ui) - ui just didnt reflect this --- app/(auth)/(tabs)/(search)/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index c7e29512..bb0c0ddd 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -55,7 +55,7 @@ export default function search() { const [settings] = useSettings(); const { jellyseerrApi } = useJellyseerr(); - const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort.DEFAULT) + const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort[JellyseerrSearchSort.DEFAULT] as unknown as JellyseerrSearchSort) const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc") const searchEngine = useMemo(() => { From e15b19deb33a1481416a4b846fd07e768e459089 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 01:06:39 -0500 Subject: [PATCH 54/93] fix: Jellyseerr filter buttons showing when library selected --- app/(auth)/(tabs)/(search)/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index bb0c0ddd..4d4686b9 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -254,7 +254,7 @@ export default function search() { } /> - {!loading && noResults && debouncedSearch.length > 0 && ( + {searchType === "Discover" && !loading && noResults && debouncedSearch.length > 0 && ( Date: Wed, 5 Mar 2025 01:24:09 -0500 Subject: [PATCH 55/93] fix: Jellyseerr slider bottom padding for posters --- components/jellyseerr/discover/Discover.tsx | 4 ++-- components/jellyseerr/discover/Slide.tsx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/jellyseerr/discover/Discover.tsx b/components/jellyseerr/discover/Discover.tsx index e5a84c1a..847b7e7b 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/jellyseerr/discover/Discover.tsx @@ -27,7 +27,7 @@ const Discover: React.FC = ({ sliders }) => { {sortedSliders.map(slide => { switch (slide.type) { case DiscoverSliderType.RECENT_REQUESTS: - return + return case DiscoverSliderType.NETWORKS: return case DiscoverSliderType.STUDIOS: @@ -40,7 +40,7 @@ const Discover: React.FC = ({ sliders }) => { case DiscoverSliderType.UPCOMING_MOVIES: case DiscoverSliderType.POPULAR_TV: case DiscoverSliderType.UPCOMING_TV: - return + return } })} diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx index f110eb15..19296fbf 100644 --- a/components/jellyseerr/discover/Slide.tsx +++ b/components/jellyseerr/discover/Slide.tsx @@ -5,9 +5,11 @@ import { Text } from "@/components/common/Text"; import { FlashList } from "@shopify/flash-list"; import {View, ViewProps} from "react-native"; import { t } from "i18next"; +import {ContentStyle} from "@shopify/flash-list/src/FlashListProps"; export interface SlideProps { slide: DiscoverSlider; + contentContainerStyle?: ContentStyle; } interface Props extends SlideProps { @@ -27,6 +29,7 @@ const Slide = ({ renderItem, keyExtractor, onEndReached, + contentContainerStyle, ...props }: PropsWithChildren & ViewProps> ) => { @@ -39,6 +42,7 @@ const Slide = ({ horizontal contentContainerStyle={{ paddingHorizontal: 16, + ...(contentContainerStyle ? contentContainerStyle : {}) }} showsHorizontalScrollIndicator={false} keyExtractor={keyExtractor} From ba9178a0f6028796ef07fcf8aba24f9070cd5cca Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 01:30:42 -0500 Subject: [PATCH 56/93] fix: Jellyseerr dont compare against tv original names during sorting --- components/jellyseerr/JellyseerrIndexPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 0363ae1e..e597f12f 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -137,7 +137,7 @@ export const JellyserrIndexPage: React.FC = ({ () => orderBy( jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], - sortingType || [t => t.originalName.toLowerCase() == searchQuery.toLowerCase()], + sortingType || [t => t.name.toLowerCase() == searchQuery.toLowerCase()], order || "desc" ), [jellyseerrResults, sortingType, order] From 89eb0d77965f442b113bb871493226ecc09e6a59 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:14:34 +0100 Subject: [PATCH 57/93] fix(https://github.com/streamyfin/streamyfin/issues/566): add Turkish (#583) --- i18n.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/i18n.ts b/i18n.ts index 973b1268..2480e384 100644 --- a/i18n.ts +++ b/i18n.ts @@ -7,6 +7,7 @@ import es from "./translations/es.json"; import fr from "./translations/fr.json"; import it from "./translations/it.json"; import ja from "./translations/ja.json"; +import tr from "./translations/tr.json"; import nl from "./translations/nl.json"; import sv from "./translations/sv.json"; import zhCN from './translations/zh-CN.json'; @@ -20,6 +21,7 @@ export const APP_LANGUAGES = [ { label: "Français", value: "fr" }, { label: "Italiano", value: "it" }, { label: "日本語", value: "ja" }, + { label: "Türkçe", value: "tr" }, { label: "Nederlands", value: "nl" }, { label: "Svenska", value: "sv" }, { label: "简体中文", value: "zh-CN" }, @@ -37,6 +39,7 @@ i18n.use(initReactI18next).init({ ja: { translation: ja }, nl: { translation: nl }, sv: { translation: sv }, + tr: { translation: tr }, "zh-CN": { translation: zhCN }, "zh-TW": { translation: zhTW }, }, From 5df021a8365506b0231f9005fcf1149f1dedfabf Mon Sep 17 00:00:00 2001 From: lostb1t Date: Wed, 5 Mar 2025 08:21:59 +0100 Subject: [PATCH 58/93] feat: Add session count to app badge (#575) --- app/_layout.tsx | 108 ++++++++++++++++----------------- hooks/useSessions.ts | 8 ++- providers/JellyfinProvider.tsx | 5 ++ utils/background-tasks.ts | 23 +++++++ utils/store.ts | 3 + 5 files changed, 91 insertions(+), 56 deletions(-) create mode 100644 utils/store.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index ad84ba0c..e3bc69d2 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,30 +2,26 @@ import "@/augmentations"; import { Platform } from "react-native"; import i18n from "@/i18n"; import { DownloadProvider } from "@/providers/DownloadProvider"; -import { - getOrSetDeviceId, - getTokenFromStorage, - JellyfinProvider, -} from "@/providers/JellyfinProvider"; +import { getOrSetDeviceId, getTokenFromStorage, JellyfinProvider, apiAtom } from "@/providers/JellyfinProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider"; import { Settings, useSettings } from "@/utils/atoms/settings"; -import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks"; +import { + BACKGROUND_FETCH_TASK, + BACKGROUND_FETCH_TASK_SESSIONS, + registerBackgroundFetchAsyncSessions, +} from "@/utils/background-tasks"; import { LogProvider, writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -const BackGroundDownloader = !Platform.isTV - ? require("@kesha-antonov/react-native-background-downloader") - : null; +const BackGroundDownloader = !Platform.isTV ? require("@kesha-antonov/react-native-background-downloader") : null; import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; +const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; import * as FileSystem from "expo-file-system"; const Notifications = !Platform.isTV ? require("expo-notifications") : null; import { router, Stack } from "expo-router"; @@ -41,6 +37,10 @@ import { SystemBars } from "react-native-edge-to-edge"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "react-native-reanimated"; import { Toaster } from "sonner-native"; +import { useAtom } from "jotai"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; +import { store } from "@/utils/store"; if (!Platform.isTV) { Notifications.setNotificationHandler({ @@ -74,20 +74,16 @@ function useNotificationObserver() { } } - Notifications.getLastNotificationResponseAsync().then( - (response: { notification: any }) => { - if (!isMounted || !response?.notification) { - return; - } - redirect(response?.notification); + Notifications.getLastNotificationResponseAsync().then((response: { notification: any }) => { + if (!isMounted || !response?.notification) { + return; } - ); + redirect(response?.notification); + }); - const subscription = Notifications.addNotificationResponseReceivedListener( - (response: { notification: any }) => { - redirect(response.notification); - } - ); + const subscription = Notifications.addNotificationResponseReceivedListener((response: { notification: any }) => { + redirect(response.notification); + }); return () => { isMounted = false; @@ -97,6 +93,22 @@ function useNotificationObserver() { } if (!Platform.isTV) { + TaskManager.defineTask(BACKGROUND_FETCH_TASK_SESSIONS, async () => { + console.log("TaskManager ~ sessions trigger"); + + const api = store.get(apiAtom); + if (api === null || api === undefined) return; + + const response = await getSessionApi(api).getSessions({ + activeWithinSeconds: 360, + }); + + const result = response.data.filter((s) => s.NowPlayingItem); + Notifications.setBadgeCountAsync(result.length); + + return BackgroundFetch.BackgroundFetchResult.NewData; + }); + TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { console.log("TaskManager ~ trigger"); @@ -109,15 +121,13 @@ if (!Platform.isTV) { const settings: Partial = JSON.parse(settingsData); const url = settings?.optimizedVersionsServerUrl; - if (!settings?.autoDownload || !url) - return BackgroundFetch.BackgroundFetchResult.NoData; + if (!settings?.autoDownload || !url) return BackgroundFetch.BackgroundFetchResult.NoData; const token = getTokenFromStorage(); const deviceId = getOrSetDeviceId(); const baseDirectory = FileSystem.documentDirectory; - if (!token || !deviceId || !baseDirectory) - return BackgroundFetch.BackgroundFetchResult.NoData; + if (!token || !deviceId || !baseDirectory) return BackgroundFetch.BackgroundFetchResult.NoData; const jobs = await getAllJobsByDeviceId({ deviceId, @@ -194,9 +204,7 @@ if (!Platform.isTV) { const checkAndRequestPermissions = async () => { try { - const hasAskedBefore = storage.getString( - "hasAskedForNotificationPermission" - ); + const hasAskedBefore = storage.getString("hasAskedForNotificationPermission"); if (hasAskedBefore !== "true") { const { status } = await Notifications.requestPermissionsAsync(); @@ -214,11 +222,7 @@ const checkAndRequestPermissions = async () => { console.log("Already asked for notification permissions before."); } } catch (error) { - writeToLog( - "ERROR", - "Error checking/requesting notification permissions:", - error - ); + writeToLog("ERROR", "Error checking/requesting notification permissions:", error); console.error("Error checking/requesting notification permissions:", error); } }; @@ -253,12 +257,11 @@ const queryClient = new QueryClient({ function Layout() { const [settings] = useSettings(); + const [user] = useAtom(userAtom); const appState = useRef(AppState.currentState); useEffect(() => { - i18n.changeLanguage( - settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en" - ); + i18n.changeLanguage(settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"); }, [settings?.preferedLanguage, i18n]); if (!Platform.isTV) { @@ -266,6 +269,11 @@ function Layout() { useEffect(() => { checkAndRequestPermissions(); + (async () => { + if (!Platform.isTV && user && user.Policy?.IsAdministrator) { + registerBackgroundFetchAsyncSessions(); + } + })(); }, []); useEffect(() => { @@ -275,24 +283,16 @@ function Layout() { ScreenOrientation.unlockAsync(); } else { // If the user has auto rotate disabled, lock the orientation to portrait - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }, [settings]); useEffect(() => { - const subscription = AppState.addEventListener( - "change", - (nextAppState) => { - if ( - appState.current.match(/inactive|background/) && - nextAppState === "active" - ) { - BackGroundDownloader.checkForExistingDownloads(); - } + const subscription = AppState.addEventListener("change", (nextAppState) => { + if (appState.current.match(/inactive|background/) && nextAppState === "active") { + BackGroundDownloader.checkForExistingDownloads(); } - ); + }); BackGroundDownloader.checkForExistingDownloads(); @@ -369,9 +369,7 @@ function Layout() { function saveDownloadedItemInfo(item: BaseItemDto) { try { const downloadedItems = storage.getString("downloadedItems"); - let items: BaseItemDto[] = downloadedItems - ? JSON.parse(downloadedItems) - : []; + let items: BaseItemDto[] = downloadedItems ? JSON.parse(downloadedItems) : []; const existingItemIndex = items.findIndex((i) => i.Id === item.Id); if (existingItemIndex !== -1) { diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index 2a2f9b09..738131fe 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -3,6 +3,8 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { useAtom } from "jotai"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { userAtom } from "@/providers/JellyfinProvider"; +import { Platform } from "react-native"; +const Notifications = !Platform.isTV ? require("expo-notifications") : null; export interface useSessionsProps { refetchInterval: number; @@ -22,9 +24,13 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = const response = await getSessionApi(api).getSessions({ activeWithinSeconds: activeWithinSeconds, }); - return response.data + + const result = response.data .filter((s) => s.NowPlayingItem) .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); + + Notifications.setBadgeCountAsync(result.length); + return result }, refetchInterval: refetchInterval, }); diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index ea473d43..37b52346 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -3,6 +3,7 @@ import { useInterval } from "@/hooks/useInterval"; import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { useSettings } from "@/utils/atoms/settings"; import { storage } from "@/utils/mmkv"; +import { store } from "@/utils/store"; import { Api, Jellyfin } from "@jellyfin/sdk"; import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserApi } from "@jellyfin/sdk/lib/utils/api"; @@ -165,6 +166,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ await refreshStreamyfinPluginSettings(); })(); }, []); + + useEffect(() => { + store.set(apiAtom, api); + }, [api]); useInterval(pollQuickConnect, isPolling ? 1000 : null); useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 0e9a408a..b066692a 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() { console.log("Error unregistering background fetch task", error); } } + +export const BACKGROUND_FETCH_TASK_SESSIONS = + "background-fetch-sessions"; + +export async function registerBackgroundFetchAsyncSessions() { + try { + BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, { + minimumInterval: 1 * 60, // 1 minutes + stopOnTerminate: false, // android only, + startOnBoot: true, // android only + }); + } catch (error) { + console.log("Error registering background fetch task", error); + } +} + +export async function unregisterBackgroundFetchAsyncSessions() { + try { + BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS); + } catch (error) { + console.log("Error unregistering background fetch task", error); + } +} \ No newline at end of file diff --git a/utils/store.ts b/utils/store.ts new file mode 100644 index 00000000..09a7aa5b --- /dev/null +++ b/utils/store.ts @@ -0,0 +1,3 @@ +import { createStore } from 'jotai'; + +export const store = createStore(); From 88efb093179b969f23eb93bf4791d12af7a88a0a Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:45:36 -0500 Subject: [PATCH 59/93] fix: fix jellyseerr search results --- components/jellyseerr/JellyseerrIndexPage.tsx | 60 ++++++------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index e597f12f..51aeb938 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -22,7 +22,6 @@ import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; import { useTranslation } from "react-i18next"; import {orderBy, uniqBy} from "lodash"; -import {useInfiniteQuery} from "@tanstack/react-query"; interface Props extends ViewProps { searchQuery: string; @@ -45,8 +44,6 @@ export const JellyserrIndexPage: React.FC = ({ const opacity = useSharedValue(1); const { t } = useTranslation(); - const [loadInitialPages, setLoadInitialPages] = useState(false) - const { data: jellyseerrDiscoverSettings, isFetching: f1, @@ -58,33 +55,27 @@ export const JellyserrIndexPage: React.FC = ({ }); const { - data: jellyseerrResultPages, + data: jellyseerrResults, isFetching: f2, - isLoading: l2, - isFetchingNextPage: n2, - hasNextPage, - fetchNextPage - } = useInfiniteQuery({ + isLoading: l2 + } = useReactNavigationQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], - queryFn: async ({pageParam}) => - jellyseerrApi?.search({ - query: new URLSearchParams(searchQuery || "").toString(), - page: Number(pageParam), - }), - enabled: !!jellyseerrApi && searchQuery.length > 0, - staleTime: 0, - initialPageParam: 1, - getNextPageParam: (lastPage, pages) => { - const firstPage = pages?.[0] - const mostRecentPage = lastPage || pages?.[pages?.length - 1] - const currentPage = mostRecentPage?.page || 1 - - return Math.min(currentPage + 1, firstPage?.totalPages || 1) + queryFn: async () => { + const params = { + query: new URLSearchParams(searchQuery || "").toString() + } + return await Promise.all([ + jellyseerrApi?.search({...params, page: 1}), + jellyseerrApi?.search({...params, page: 2}), + jellyseerrApi?.search({...params, page: 3}), + jellyseerrApi?.search({...params, page: 4}) + ]).then(all => uniqBy(all.flatMap(v => v?.results || []), "id")) }, + enabled: !!jellyseerrApi && searchQuery.length > 0, }); useAnimatedReaction( - () => f1 || f2 || l1 || l2 || n2, + () => f1 || f2 || l1 || l2, (isLoading) => { if (isLoading) { opacity.value = withTiming(1, { duration: 200 }); @@ -109,20 +100,6 @@ export const JellyserrIndexPage: React.FC = ({ [sortType, order] ) - const jellyseerrResults = useMemo( - () => { - const lastPage = jellyseerrResultPages?.pages?.[jellyseerrResultPages?.pages?.length - 1] - - if ((lastPage?.page || 0) % 5 !== 0 && hasNextPage && !loadInitialPages) { - fetchNextPage() - setLoadInitialPages(lastPage?.page === 4 || (lastPage !== undefined && lastPage.totalPages == lastPage.page)) - } - - return uniqBy(jellyseerrResultPages?.pages?.flatMap?.(page => page?.results || []), "id") - }, - [jellyseerrResultPages, fetchNextPage, hasNextPage] - ); - const jellyseerrMovieResults = useMemo( () => orderBy( @@ -162,7 +139,7 @@ export const JellyserrIndexPage: React.FC = ({ return ( - + {!jellyseerrMovieResults?.length && !jellyseerrTvResults?.length && @@ -170,8 +147,7 @@ export const JellyserrIndexPage: React.FC = ({ !f1 && !f2 && !l1 && - !l2 && - !loadInitialPages && ( + !l2 && ( {t("search.no_results_found_for")} @@ -182,7 +158,7 @@ export const JellyserrIndexPage: React.FC = ({ )} - + Date: Wed, 5 Mar 2025 20:07:21 -0500 Subject: [PATCH 60/93] fix: Recent request slider initial loading --- components/common/JellyseerrItemRouter.tsx | 10 +++++++--- .../discover/RecentRequestsSlide.tsx | 4 ++-- components/posters/JellyseerrPoster.tsx | 20 ++++++++++--------- hooks/useJellyseerr.ts | 10 +++++----- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index b132e437..ec1a1801 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -13,7 +13,7 @@ import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; interface Props extends TouchableOpacityProps { - result: MovieResult | TvResult | MovieDetails | TvDetails; + result?: MovieResult | TvResult | MovieDetails | TvDetails; mediaTitle: string; releaseYear: number; canRequest: boolean; @@ -47,11 +47,13 @@ export const TouchableJellyseerrRouter: React.FC> = ({ }, [jellyseerrApi, jellyseerrUser]); const request = useCallback( - () => + () => { + if (!result) return; requestMedia(mediaTitle, { mediaId: result.id, mediaType, - }), + }) + }, [jellyseerrApi, result] ); @@ -62,6 +64,8 @@ export const TouchableJellyseerrRouter: React.FC> = ({ { + if (!result) return; + // @ts-ignore router.push({ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index 9a9902a6..dce6d7b9 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -34,7 +34,7 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { }); return ( - details && + ) } @@ -50,7 +50,7 @@ const RecentRequestsSlide: React.FC = ({ slide, ...props }); return ( - requests && ( + requests && requests.results.length > 0 && ( = ({ }; const backdropSrc = useMemo( - () => jellyseerrApi?.imageProxy(item.backdropPath, "w1920_and_h800_multi_faces"), + () => jellyseerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"), [item, jellyseerrApi, horizontal] ); const posterSrc = useMemo( - () => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face",), + () => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face",), [item, jellyseerrApi, horizontal] ); @@ -122,8 +122,8 @@ const JellyseerrPoster: React.FC = ({ = ({ /> - - {title} - {releaseYear} - + {item && ( + + {title} + {releaseYear} + + )} ); }; diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 7c860180..f7400dc1 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -464,22 +464,22 @@ export const useJellyseerr = () => { ) }; - const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { + const getTitle = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { return isJellyseerrResult(item) ? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name) - : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) + : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) }; - const getYear = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { + const getYear = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { return new Date(( isJellyseerrResult(item) ? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate) - : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate)) + : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate)) || "" )?.getFullYear?.() }; - const getMediaType = (item: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => { + const getMediaType = (item?: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => { return isJellyseerrResult(item) ? item.mediaType : item?.mediaInfo?.mediaType From d33baf07d38ff3e79be76f07b764646f46977367 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:16:42 -0500 Subject: [PATCH 61/93] fix: Recent requests requester name --- components/posters/JellyseerrPoster.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 10d1f3c2..57ae1c44 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -17,7 +17,7 @@ import {useTranslation} from "react-i18next"; import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; import {Colors} from "@/constants/Colors"; -import {Tags} from "@/components/GenreTags"; +import {Tag, Tags} from "@/components/GenreTags"; interface Props extends ViewProps { item?: MovieResult | TvResult | MovieDetails | TvDetails; @@ -156,12 +156,10 @@ const JellyseerrPoster: React.FC = ({ )} - - {mediaRequest?.requestedBy.displayName} - + {requestedSeasons.length > 0 && ( Date: Wed, 5 Mar 2025 20:23:28 -0500 Subject: [PATCH 62/93] fix: Flashlist container not changing height --- components/posters/JellyseerrPoster.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 57ae1c44..25c16d4f 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -182,12 +182,10 @@ const JellyseerrPoster: React.FC = ({ /> - {item && ( - - {title} - {releaseYear} - - )} + + {title || ""} + {releaseYear || ""} + ); }; From 3d7889e19a52bd94b28832c5a3ba63e3a330fdb2 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 7 Mar 2025 07:53:50 +0100 Subject: [PATCH 63/93] fix: orientation lock being activated even when auto rotate is on --- app/(auth)/player/_layout.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index c06c05a2..a0ff4382 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -1,30 +1,30 @@ -import { Stack } from "expo-router"; -import React, { useEffect } from "react"; -import { SystemBars } from "react-native-edge-to-edge"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useSettings } from "@/utils/atoms/settings"; +import { Stack } from "expo-router"; +import React, { useLayoutEffect } from "react"; import { Platform } from "react-native"; +import { SystemBars } from "react-native-edge-to-edge"; export default function Layout() { const [settings] = useSettings(); - useEffect(() => { + useLayoutEffect(() => { if (Platform.isTV) return; - if (settings.defaultVideoOrientation) { + if (!settings.followDeviceOrientation && settings.defaultVideoOrientation) { ScreenOrientation.lockAsync(settings.defaultVideoOrientation); } return () => { if (Platform.isTV) return; - if (settings.autoRotate === true) { + if (settings.followDeviceOrientation === true) { ScreenOrientation.unlockAsync(); } else { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }; - }, [settings.defaultVideoOrientation, settings.autoRotate]); + }, [settings.autoDownload, settings.defaultVideoOrientation]); return ( <> From 887f30e739710eb113274578ad66e36f42518d5e Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 7 Mar 2025 07:54:13 +0100 Subject: [PATCH 64/93] fix: rename auto rotate to follow device orientation --- app/_layout.tsx | 4 +- components/settings/OtherSettings.tsx | 33 +++++--- hooks/useOrientationSettings.ts | 12 +-- translations/de.json | 10 +-- translations/en.json | 8 +- translations/es.json | 10 +-- translations/fr.json | 9 +-- translations/it.json | 4 +- translations/ja.json | 4 +- translations/nl.json | 4 +- translations/tr.json | 4 +- translations/zh-CN.json | 4 +- translations/zh-TW.json | 4 +- utils/atoms/settings.ts | 104 ++++++++++---------------- 14 files changed, 98 insertions(+), 116 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index e3bc69d2..8d7aed55 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -105,7 +105,7 @@ if (!Platform.isTV) { const result = response.data.filter((s) => s.NowPlayingItem); Notifications.setBadgeCountAsync(result.length); - + return BackgroundFetch.BackgroundFetchResult.NewData; }); @@ -279,7 +279,7 @@ function Layout() { useEffect(() => { // If the user has auto rotate enabled, unlock the orientation if (Platform.isTV) return; - if (settings.autoRotate === true) { + if (settings.followDeviceOrientation === true) { ScreenOrientation.unlockAsync(); } else { // If the user has auto rotate disabled, lock the orientation to portrait diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 932ad967..896f2399 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -60,7 +60,7 @@ export const OtherSettings: React.FC = () => { const disabled = useMemo( () => - pluginSettings?.autoRotate?.locked === true && + pluginSettings?.followDeviceOrientation?.locked === true && pluginSettings?.defaultVideoOrientation?.locked === true && pluginSettings?.safeAreaInControlsEnabled?.locked === true && pluginSettings?.showCustomMenuLinks?.locked === true && @@ -76,32 +76,47 @@ export const OtherSettings: React.FC = () => { ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, ]; + const orientationTranslations = useMemo( + () => ({ + [ScreenOrientation.OrientationLock.DEFAULT]: "home.settings.other.orientations.DEFAULT", + [ScreenOrientation.OrientationLock.PORTRAIT_UP]: "home.settings.other.orientations.PORTRAIT_UP", + [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "home.settings.other.orientations.LANDSCAPE_LEFT", + [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "home.settings.other.orientations.LANDSCAPE_RIGHT", + }), + [] + ); + if (!settings) return null; return ( - + updateSettings({ autoRotate: value })} + value={settings.followDeviceOrientation} + disabled={pluginSettings?.followDeviceOrientation?.locked} + onValueChange={(value) => updateSettings({ followDeviceOrientation: value })} /> ScreenOrientationEnum[item]} + titleExtractor={(item) => t(ScreenOrientationEnum[item])} title={ - {t(ScreenOrientationEnum[settings.defaultVideoOrientation])} + {t( + orientationTranslations[settings.defaultVideoOrientation as keyof typeof orientationTranslations] + ) || "Unknown Orientation"} diff --git a/hooks/useOrientationSettings.ts b/hooks/useOrientationSettings.ts index 7b657d77..b58a06ce 100644 --- a/hooks/useOrientationSettings.ts +++ b/hooks/useOrientationSettings.ts @@ -9,21 +9,17 @@ export const useOrientationSettings = () => { const [settings] = useSettings(); useEffect(() => { - if (settings?.autoRotate) { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT - ); + if (settings?.followDeviceOrientation) { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT); } else if (settings?.defaultVideoOrientation) { ScreenOrientation.lockAsync(settings.defaultVideoOrientation); } return () => { - if (settings?.autoRotate) { + if (settings?.followDeviceOrientation) { ScreenOrientation.unlockAsync(); } else { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }; }, [settings]); diff --git a/translations/de.json b/translations/de.json index 19258147..ac1b2759 100644 --- a/translations/de.json +++ b/translations/de.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Sonstiges", - "auto_rotate": "Automatische Drehung", + "follow_device_orientation": "Automatische Drehung", "video_orientation": "Videoausrichtung", "orientation": "Ausrichtung", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Andere", "UNKNOWN": "Unbekannt" }, - "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", @@ -152,7 +152,7 @@ "default": "Standard", "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", - "url":"URL", + "url": "URL", "server_url_placeholder": "http(s)://domain.org:port" }, "plugins": { @@ -215,7 +215,7 @@ "app_language_description": "Wähle die Sprache für die App aus.", "system": "System" }, - "toasts":{ + "toasts": { "error_deleting_files": "Fehler beim Löschen von Dateien", "background_downloads_enabled": "Hintergrunddownloads aktiviert", "background_downloads_disabled": "Hintergrunddownloads deaktiviert", @@ -412,7 +412,7 @@ "for_kids": "Für Kinder", "news": "Nachrichten" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Bestätigen", "cancel": "Abbrechen", "yes": "Ja", diff --git a/translations/en.json b/translations/en.json index e804e5cb..4c454f9a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Other", - "auto_rotate": "Auto rotate", + "follow_device_orientation": "Follow device orientation", "video_orientation": "Video orientation", "orientation": "Orientation", "orientations": { @@ -138,7 +138,7 @@ "hide_libraries": "Hide Libraries", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality", + "default_quality": "Default quality" }, "downloads": { "downloads_title": "Downloads", @@ -222,7 +222,7 @@ "connected": "Connected", "could_not_connect": "Could not connect", "invalid_url": "Invalid URL" - }, + } }, "sessions": { "title": "Sessions", @@ -472,4 +472,4 @@ "custom_links": "Custom Links", "favorites": "Favorites" } -} \ No newline at end of file +} diff --git a/translations/es.json b/translations/es.json index 463d86f2..95e30124 100644 --- a/translations/es.json +++ b/translations/es.json @@ -20,7 +20,7 @@ "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", "there_is_a_server_error": "Hay un error en el servidor", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" }, "server": { "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", @@ -113,7 +113,7 @@ }, "other": { "other_title": "Otros", - "auto_rotate": "Rotación automática", + "follow_device_orientation": "Rotación automática", "video_orientation": "Orientación de vídeo", "orientation": "Orientación", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Otra", "UNKNOWN": "Desconocida" }, - "safe_area_in_controls": "Área segura en controles", + "safe_area_in_controls": "Área segura en controles", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", @@ -215,7 +215,7 @@ "app_language_description": "Selecciona el idioma de la app.", "system": "Sistema" }, - "toasts":{ + "toasts": { "error_deleting_files": "Error al eliminar archivos", "background_downloads_enabled": "Descargas en segundo plano habilitadas", "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", @@ -412,7 +412,7 @@ "for_kids": "Para niños", "news": "Noticias" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirmar", "cancel": "Cancelar", "yes": "Sí", diff --git a/translations/fr.json b/translations/fr.json index 6df653ff..dbd5b5ef 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Autres", - "auto_rotate": "Rotation automatique", + "follow_device_orientation": "Rotation automatique", "video_orientation": "Orientation vidéo", "orientation": "Orientation", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Autre", "UNKNOWN": "Inconnu" }, - "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "safe_area_in_controls": "Zone de sécurité dans les contrôles", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", @@ -139,7 +139,6 @@ "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.", "disable_haptic_feedback": "Désactiver le retour haptique", "default_quality": "Qualité par défaut" - }, "downloads": { "downloads_title": "Téléchargements", @@ -216,7 +215,7 @@ "app_language_description": "Sélectionnez la langue de l'application", "system": "Système" }, - "toasts":{ + "toasts": { "error_deleting_files": "Erreur lors de la suppression des fichiers", "background_downloads_enabled": "Téléchargements en arrière-plan activés", "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", @@ -413,7 +412,7 @@ "for_kids": "Pour enfants", "news": "Actualités" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirmer", "cancel": "Annuler", "yes": "Oui", diff --git a/translations/it.json b/translations/it.json index 87882704..98b6a903 100644 --- a/translations/it.json +++ b/translations/it.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Altro", - "auto_rotate": "Rotazione automatica", + "follow_device_orientation": "Rotazione automatica", "video_orientation": "Orientamento del video", "orientation": "Orientamento", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Altro", "UNKNOWN": "Sconosciuto" }, - "safe_area_in_controls": "Area sicura per i controlli", + "safe_area_in_controls": "Area sicura per i controlli", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/translations/ja.json b/translations/ja.json index dfdbc59d..7cf14a65 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "その他", - "auto_rotate": "画面の自動回転", + "follow_device_orientation": "画面の自動回転", "video_orientation": "動画の向き", "orientation": "向き", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "その他", "UNKNOWN": "不明" }, - "safe_area_in_controls": "コントロールの安全エリア", + "safe_area_in_controls": "コントロールの安全エリア", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/translations/nl.json b/translations/nl.json index 1f87dfc8..54c39e34 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Andere", - "auto_rotate": "Automatisch draaien", + "follow_device_orientation": "Automatisch draaien", "video_orientation": "Video oriëntatie", "orientation": "Oriëntatie", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Andere", "UNKNOWN": "Onbekend" }, - "safe_area_in_controls": "Veilig gebied in bedieningen", + "safe_area_in_controls": "Veilig gebied in bedieningen", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/translations/tr.json b/translations/tr.json index 47f6fc02..f9e0e45f 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Diğer", - "auto_rotate": "Otomatik Döndürme", + "follow_device_orientation": "Otomatik Döndürme", "video_orientation": "Video Yönü", "orientation": "Yön", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "Diğer", "UNKNOWN": "Bilinmeyen" }, - "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "safe_area_in_controls": "Kontrollerde Güvenli Alan", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/translations/zh-CN.json b/translations/zh-CN.json index ad2d0468..aad0efd9 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "其他", - "auto_rotate": "自动旋转", + "follow_device_orientation": "自动旋转", "video_orientation": "视频方向", "orientation": "方向", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "其他", "UNKNOWN": "未知" }, - "safe_area_in_controls": "控制中的安全区域", + "safe_area_in_controls": "控制中的安全区域", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/translations/zh-TW.json b/translations/zh-TW.json index d4f42fe3..cfe2f7ce 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "其他", - "auto_rotate": "自動旋轉", + "follow_device_orientation": "自動旋轉", "video_orientation": "影片方向", "orientation": "方向", "orientations": { @@ -128,7 +128,7 @@ "OTHER": "其他", "UNKNOWN": "未知" }, - "safe_area_in_controls": "控制中的安全區域", + "safe_area_in_controls": "控制中的安全區域", "video_player": "Video player", "video_players": { "VLC_3": "VLC 3", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index a1ba7d0b..34b2f776 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -14,7 +14,7 @@ import { import { Bitrate, BITRATES } from "@/components/BitrateSelector"; import { apiAtom } from "@/providers/JellyfinProvider"; import { writeInfoLog } from "@/utils/log"; -import {Video} from "@/utils/jellyseerr/server/models/Movie"; +import { Video } from "@/utils/jellyseerr/server/models/Movie"; const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; @@ -26,30 +26,17 @@ export type DownloadOption = { value: DownloadQuality; }; -export const ScreenOrientationEnum: Record< - ScreenOrientation.OrientationLock, - string -> = { - [ScreenOrientation.OrientationLock.DEFAULT]: - "home.settings.other.orientations.DEFAULT", - [ScreenOrientation.OrientationLock.ALL]: - "home.settings.other.orientations.ALL", - [ScreenOrientation.OrientationLock.PORTRAIT]: - "home.settings.other.orientations.PORTRAIT", - [ScreenOrientation.OrientationLock.PORTRAIT_UP]: - "home.settings.other.orientations.PORTRAIT_UP", - [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: - "home.settings.other.orientations.PORTRAIT_DOWN", - [ScreenOrientation.OrientationLock.LANDSCAPE]: - "home.settings.other.orientations.LANDSCAPE", - [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: - "home.settings.other.orientations.LANDSCAPE_LEFT", - [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: - "home.settings.other.orientations.LANDSCAPE_RIGHT", - [ScreenOrientation.OrientationLock.OTHER]: - "home.settings.other.orientations.OTHER", - [ScreenOrientation.OrientationLock.UNKNOWN]: - "home.settings.other.orientations.UNKNOWN", +export const ScreenOrientationEnum: Record = { + [ScreenOrientation.OrientationLock.DEFAULT]: "home.settings.other.orientations.DEFAULT", + [ScreenOrientation.OrientationLock.ALL]: "home.settings.other.orientations.ALL", + [ScreenOrientation.OrientationLock.PORTRAIT]: "home.settings.other.orientations.PORTRAIT", + [ScreenOrientation.OrientationLock.PORTRAIT_UP]: "home.settings.other.orientations.PORTRAIT_UP", + [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "home.settings.other.orientations.PORTRAIT_DOWN", + [ScreenOrientation.OrientationLock.LANDSCAPE]: "home.settings.other.orientations.LANDSCAPE", + [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "home.settings.other.orientations.LANDSCAPE_LEFT", + [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "home.settings.other.orientations.LANDSCAPE_RIGHT", + [ScreenOrientation.OrientationLock.OTHER]: "home.settings.other.orientations.OTHER", + [ScreenOrientation.OrientationLock.UNKNOWN]: "home.settings.other.orientations.UNKNOWN", }; export const DownloadOptions: DownloadOption[] = [ @@ -116,12 +103,12 @@ export type HomeSectionNextUpResolver = { export enum VideoPlayer { // NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted VLC_3, - VLC_4 + VLC_4, } export type Settings = { home?: Home | null; - autoRotate?: boolean; + followDeviceOrientation?: boolean; forceLandscapeInVideoPlayer?: boolean; deviceProfile?: "Expo" | "Native" | "Old"; mediaListCollectionIds?: string[]; @@ -170,7 +157,7 @@ export type StreamyfinPluginConfig = { const defaultValues: Settings = { home: null, - autoRotate: true, + followDeviceOrientation: true, forceLandscapeInVideoPlayer: false, deviceProfile: "Expo", mediaListCollectionIds: [], @@ -214,8 +201,7 @@ const defaultValues: Settings = { const loadSettings = (): Partial => { try { const jsonValue = storage.getString("settings"); - const loadedValues: Partial = - jsonValue != null ? JSON.parse(jsonValue) : {}; + const loadedValues: Partial = jsonValue != null ? JSON.parse(jsonValue) : {}; return loadedValues; } catch (error) { @@ -237,9 +223,7 @@ const saveSettings = (settings: Settings) => { }; export const settingsAtom = atom | null>(null); -export const pluginSettingsAtom = atom( - storage.get(STREAMYFIN_PLUGIN_SETTINGS) -); +export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); export const useSettings = () => { const [api] = useAtom(apiAtom); @@ -275,12 +259,13 @@ export const useSettings = () => { }, [api]); const updateSettings = (update: Partial) => { - if (settings) { - const newSettings = { ..._settings, ...update }; + if (!_settings) return; + const hasChanges = Object.entries(update).some(([key, value]) => _settings[key as keyof Settings] !== value); + if (hasChanges) { + // Merge default settings, current settings, and updates to ensure all required properties exist + const newSettings = { ...defaultValues, ..._settings, ...update } as Settings; setSettings(newSettings); - - // @ts-expect-error saveSettings(newSettings); } }; @@ -290,31 +275,24 @@ export const useSettings = () => { // use user settings first and fallback on admin setting if required. const settings: Settings = useMemo(() => { let unlockedPluginDefaults = {} as Settings; - const overrideSettings = Object.entries(pluginSettings || {}).reduce( - (acc, [key, setting]) => { - if (setting) { - const { value, locked } = setting; + const overrideSettings = Object.entries(pluginSettings || {}).reduce((acc, [key, setting]) => { + if (setting) { + const { value, locked } = setting; - // Make sure we override default settings with plugin settings when they are not locked. - // Admin decided what users defaults should be and grants them the ability to change them too. - if ( - locked === false && - value && - _settings?.[key as keyof Settings] !== value - ) { - unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { - [key as keyof Settings]: value, - }); - } - - acc = Object.assign(acc, { - [key]: locked ? value : _settings?.[key as keyof Settings] ?? value, + // Make sure we override default settings with plugin settings when they are not locked. + // Admin decided what users defaults should be and grants them the ability to change them too. + if (locked === false && value && _settings?.[key as keyof Settings] !== value) { + unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { + [key as keyof Settings]: value, }); } - return acc; - }, - {} as Settings - ); + + acc = Object.assign(acc, { + [key]: locked ? value : _settings?.[key as keyof Settings] ?? value, + }); + } + return acc; + }, {} as Settings); return { ...defaultValues, @@ -323,11 +301,5 @@ export const useSettings = () => { }; }, [_settings, pluginSettings]); - return [ - settings, - updateSettings, - pluginSettings, - setPluginSettings, - refreshStreamyfinPluginSettings, - ] as const; + return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const; }; From 81535894e1122d8ad5fee3d9f9ec48f1be31b95f Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 8 Mar 2025 08:27:18 +0100 Subject: [PATCH 65/93] fix: unwanted rotate to portrait after a long time of watching --- app/(auth)/player/_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index a0ff4382..745f9bbe 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -24,7 +24,7 @@ export default function Layout() { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }; - }, [settings.autoDownload, settings.defaultVideoOrientation]); + }); return ( <> From ef355b1f04953c71be8a3ad69e3efbfdff0d4a4a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 8 Mar 2025 08:27:28 +0100 Subject: [PATCH 66/93] fix: remove unused react-native-video --- components/video-player/controls/Controls.tsx | 219 +++++++----------- 1 file changed, 80 insertions(+), 139 deletions(-) diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 36889f0f..94914307 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -1,44 +1,43 @@ -import {Text} from "@/components/common/Text"; -import {Loader} from "@/components/Loader"; -import {useAdjacentItems} from "@/hooks/useAdjacentEpisodes"; -import {useCreditSkipper} from "@/hooks/useCreditSkipper"; -import {useHaptic} from "@/hooks/useHaptic"; -import {useIntroSkipper} from "@/hooks/useIntroSkipper"; -import {useTrickplay} from "@/hooks/useTrickplay"; -import {TrackInfo, VlcPlayerViewRef,} from "@/modules/VlcPlayer.types"; -import {apiAtom} from "@/providers/JellyfinProvider"; -import {useSettings, VideoPlayer} from "@/utils/atoms/settings"; -import {getDefaultPlaySettings,} from "@/utils/jellyfin/getDefaultPlaySettings"; -import {getItemById} from "@/utils/jellyfin/user-library/getItemById"; -import {writeToLog} from "@/utils/log"; -import {formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds,} from "@/utils/time"; -import {Ionicons, MaterialIcons} from "@expo/vector-icons"; -import {BaseItemDto, MediaSourceInfo,} from "@jellyfin/sdk/lib/generated-client"; -import {Image} from "expo-image"; -import {useLocalSearchParams, useRouter} from "expo-router"; +import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; +import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes"; +import { useCreditSkipper } from "@/hooks/useCreditSkipper"; +import { useHaptic } from "@/hooks/useHaptic"; +import { useIntroSkipper } from "@/hooks/useIntroSkipper"; +import { useTrickplay } from "@/hooks/useTrickplay"; +import { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import {useAtom} from "jotai"; -import {debounce} from "lodash"; -import React, {useCallback, useEffect, useRef, useState} from "react"; -import {Platform, TouchableOpacity, useWindowDimensions, View,} from "react-native"; -import {Slider} from "react-native-awesome-slider"; -import {runOnJS, SharedValue, useAnimatedReaction, useSharedValue,} from "react-native-reanimated"; -import {useSafeAreaInsets} from "react-native-safe-area-context"; -import {VideoRef} from "react-native-video"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { useSettings, VideoPlayer } from "@/utils/atoms/settings"; +import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; +import { getItemById } from "@/utils/jellyfin/user-library/getItemById"; +import { writeToLog } from "@/utils/log"; +import { formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds } from "@/utils/time"; +import { Ionicons, MaterialIcons } from "@expo/vector-icons"; +import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; +import { Image } from "expo-image"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useAtom } from "jotai"; +import { debounce } from "lodash"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Platform, TouchableOpacity, useWindowDimensions, View } from "react-native"; +import { Slider } from "react-native-awesome-slider"; +import { runOnJS, SharedValue, useAnimatedReaction, useSharedValue } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import AudioSlider from "./AudioSlider"; import BrightnessSlider from "./BrightnessSlider"; -import {ControlProvider} from "./contexts/ControlContext"; -import {VideoProvider} from "./contexts/VideoContext"; +import { ControlProvider } from "./contexts/ControlContext"; +import { VideoProvider } from "./contexts/VideoContext"; import DropdownView from "./dropdown/DropdownView"; -import {EpisodeList} from "./EpisodeList"; +import { EpisodeList } from "./EpisodeList"; import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton"; import SkipButton from "./SkipButton"; -import {useControlsTimeout} from "./useControlsTimeout"; -import {VideoTouchOverlay} from "./VideoTouchOverlay"; +import { useControlsTimeout } from "./useControlsTimeout"; +import { VideoTouchOverlay } from "./VideoTouchOverlay"; interface Props { item: BaseItemDto; - videoRef: React.MutableRefObject; + videoRef: React.MutableRefObject; isPlaying: boolean; isSeeking: SharedValue; cacheProgress: SharedValue; @@ -68,32 +67,32 @@ interface Props { const CONTROLS_TIMEOUT = 4000; export const Controls: React.FC = ({ - item, - seek, - startPictureInPicture, - play, - pause, - togglePlay, - isPlaying, - isSeeking, - progress, - isBuffering, - cacheProgress, - showControls, - setShowControls, - ignoreSafeAreas, - setIgnoreSafeAreas, - mediaSource, - isVideoLoaded, - getAudioTracks, - getSubtitleTracks, - setSubtitleURL, - setSubtitleTrack, - setAudioTrack, - offline = false, - enableTrickplay = true, - isVlc = false, - }) => { + item, + seek, + startPictureInPicture, + play, + pause, + togglePlay, + isPlaying, + isSeeking, + progress, + isBuffering, + cacheProgress, + showControls, + setShowControls, + ignoreSafeAreas, + setIgnoreSafeAreas, + mediaSource, + isVideoLoaded, + getAudioTracks, + getSubtitleTracks, + setSubtitleURL, + setSubtitleTrack, + setAudioTrack, + offline = false, + enableTrickplay = true, + isVlc = false, +}) => { const [settings] = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); @@ -107,12 +106,10 @@ export const Controls: React.FC = ({ const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const { previousItem, nextItem } = useAdjacentItems({ item }); - const { - trickPlayUrl, - calculateTrickplayUrl, - trickplayInfo, - prefetchAllTrickplayImages, - } = useTrickplay(item, !offline && enableTrickplay); + const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo, prefetchAllTrickplayImages } = useTrickplay( + item, + !offline && enableTrickplay + ); const [currentTime, setCurrentTime] = useState(0); const [remainingTime, setRemainingTime] = useState(Infinity); @@ -134,9 +131,7 @@ export const Controls: React.FC = ({ progress.value = isVlc ? ticksToMs(item?.UserData?.PlaybackPositionTicks) : item?.UserData?.PlaybackPositionTicks || 0; - max.value = isVlc - ? ticksToMs(item.RunTimeTicks || 0) - : item.RunTimeTicks || 0; + max.value = isVlc ? ticksToMs(item.RunTimeTicks || 0) : item.RunTimeTicks || 0; } }, [item, isVlc]); @@ -146,13 +141,7 @@ export const Controls: React.FC = ({ subtitleIndex: string; }>(); - const { showSkipButton, skipIntro } = useIntroSkipper( - offline ? undefined : item.Id, - currentTime, - seek, - play, - isVlc - ); + const { showSkipButton, skipIntro } = useIntroSkipper(offline ? undefined : item.Id, currentTime, seek, play, isVlc); const { showSkipCreditButton, skipCredit } = useCreditSkipper( offline ? undefined : item.Id, @@ -177,12 +166,7 @@ export const Controls: React.FC = ({ mediaSource: newMediaSource, audioIndex: defaultAudioIndex, subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - item, - settings, - previousIndexes, - mediaSource ?? undefined - ); + } = getDefaultPlaySettings(item, settings, previousIndexes, mediaSource ?? undefined); const queryParams = new URLSearchParams({ itemId: item.Id ?? "", @@ -220,9 +204,7 @@ export const Controls: React.FC = ({ const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { const current = isVlc ? currentProgress : ticksToSeconds(currentProgress); - const remaining = isVlc - ? maxValue - currentProgress - : ticksToSeconds(maxValue - currentProgress); + const remaining = isVlc ? maxValue - currentProgress : ticksToSeconds(maxValue - currentProgress); setCurrentTime(current); setRemainingTime(remaining); @@ -384,13 +366,8 @@ export const Controls: React.FC = ({ cachePolicy={"memory-disk"} style={{ width: 150 * trickplayInfo?.data.TileWidth!, - height: - (150 / trickplayInfo.aspectRatio!) * - trickplayInfo?.data.TileHeight!, - transform: [ - { translateX: -x * tileWidth }, - { translateY: -y * tileHeight }, - ], + height: (150 / trickplayInfo.aspectRatio!) * trickplayInfo?.data.TileHeight!, + transform: [{ translateX: -x * tileWidth }, { translateY: -y * tileHeight }], resizeMode: "cover", }} source={{ uri: url }} @@ -403,9 +380,9 @@ export const Controls: React.FC = ({ fontSize: 16, }} > - {`${time.hours > 0 ? `${time.hours}:` : ""}${ - time.minutes < 10 ? `0${time.minutes}` : time.minutes - }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`} + {`${time.hours > 0 ? `${time.hours}:` : ""}${time.minutes < 10 ? `0${time.minutes}` : time.minutes}:${ + time.seconds < 10 ? `0${time.seconds}` : time.seconds + }`} ); @@ -413,24 +390,14 @@ export const Controls: React.FC = ({ const onClose = async () => { lightHapticFeedback(); - await ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); + await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); router.back(); }; return ( - + {episodeView ? ( - setEpisodeView(false)} - goToItem={goToItem} - /> + setEpisodeView(false)} goToItem={goToItem} /> ) : ( <> = ({ position: "absolute", top: settings?.safeAreaInControlsEnabled ? insets.top : 0, right: settings?.safeAreaInControlsEnabled ? insets.right : 0, - width: settings?.safeAreaInControlsEnabled - ? screenWidth - insets.left - insets.right - : screenWidth, + width: settings?.safeAreaInControlsEnabled ? screenWidth - insets.left - insets.right : screenWidth, opacity: showControls ? 1 : 0, }, ]} @@ -516,11 +481,7 @@ export const Controls: React.FC = ({ onPress={toggleIgnoreSafeAreas} className="aspect-square flex flex-col rounded-xl items-center justify-center p-2" > - + {/* )} */} = ({ )} {item?.Name} - {item?.Type === "Movie" && ( - - {item?.ProductionYear} - - )} - {item?.Type === "Audio" && ( - {item?.Album} - )} + {item?.Type === "Movie" && {item?.ProductionYear}} + {item?.Type === "Audio" && {item?.Album}} - - + + From 7cdf0e535537e8793f45fd4c75e54b3d9c387a8a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 8 Mar 2025 10:13:04 +0100 Subject: [PATCH 67/93] chore --- app/_layout.tsx | 9 ++++++--- hooks/useOrientationSettings.ts | 26 -------------------------- 2 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 hooks/useOrientationSettings.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 8d7aed55..cceafcfd 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -24,7 +24,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; import * as FileSystem from "expo-file-system"; const Notifications = !Platform.isTV ? require("expo-notifications") : null; -import { router, Stack } from "expo-router"; +import { router, Stack, useSegments } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; @@ -259,6 +259,7 @@ function Layout() { const [settings] = useSettings(); const [user] = useAtom(userAtom); const appState = useRef(AppState.currentState); + const segments = useSegments(); useEffect(() => { i18n.changeLanguage(settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"); @@ -277,15 +278,17 @@ function Layout() { }, []); useEffect(() => { - // If the user has auto rotate enabled, unlock the orientation if (Platform.isTV) return; + if ("direct-player" in segments) return; + + // If the user has auto rotate enabled, unlock the orientation if (settings.followDeviceOrientation === true) { ScreenOrientation.unlockAsync(); } else { // If the user has auto rotate disabled, lock the orientation to portrait ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } - }, [settings]); + }, [settings.followDeviceOrientation, segments]); useEffect(() => { const subscription = AppState.addEventListener("change", (nextAppState) => { diff --git a/hooks/useOrientationSettings.ts b/hooks/useOrientationSettings.ts deleted file mode 100644 index b58a06ce..00000000 --- a/hooks/useOrientationSettings.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useSettings } from "@/utils/atoms/settings"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { useEffect } from "react"; -import { Platform } from "react-native"; - -export const useOrientationSettings = () => { - if (Platform.isTV) return; - - const [settings] = useSettings(); - - useEffect(() => { - if (settings?.followDeviceOrientation) { - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT); - } else if (settings?.defaultVideoOrientation) { - ScreenOrientation.lockAsync(settings.defaultVideoOrientation); - } - - return () => { - if (settings?.followDeviceOrientation) { - ScreenOrientation.unlockAsync(); - } else { - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); - } - }; - }, [settings]); -}; From 96e3362f43b539358bd42ed6ee8ffcafb7edc13b Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 8 Mar 2025 10:33:14 +0100 Subject: [PATCH 68/93] fix: rotate bugs in video player --- app/_layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index cceafcfd..8dfe0786 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -279,7 +279,9 @@ function Layout() { useEffect(() => { if (Platform.isTV) return; - if ("direct-player" in segments) return; + if (segments.includes("direct-player" as never)) { + return; + } // If the user has auto rotate enabled, unlock the orientation if (settings.followDeviceOrientation === true) { From c29b2cb8da90aa25d21b257db66f704726e23543 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 10 Mar 2025 15:51:18 +0100 Subject: [PATCH 69/93] feat: Add location to session --- app/(auth)/(tabs)/(home)/sessions/index.tsx | 98 +++++++-------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index 5c2c1992..c797b83e 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -13,13 +13,9 @@ import { useInterval } from "@/hooks/useInterval"; import React, { useEffect, useMemo, useState } from "react"; import { formatTimeString } from "@/utils/time"; import { formatBitrate } from "@/utils/bitrate"; -import { - Ionicons, - Entypo, - AntDesign, - MaterialCommunityIcons, -} from "@expo/vector-icons"; +import { Ionicons, Entypo, AntDesign, MaterialCommunityIcons } from "@expo/vector-icons"; import { Badge } from "@/components/Badge"; +import { useQuery } from "@tanstack/react-query"; export default function page() { const { sessions, isLoading } = useSessions({} as useSessionsProps); @@ -35,9 +31,7 @@ export default function page() { if (!sessions || sessions.length == 0) return ( - - {t("home.sessions.no_active_sessions")} - + {t("home.sessions.no_active_sessions")} ); @@ -76,44 +70,42 @@ const SessionCard = ({ session }: SessionCardProps) => { } return Math.round( - (100 / session.NowPlayingItem?.RunTimeTicks) * - (session.NowPlayingItem?.RunTimeTicks - remainingTicks) + (100 / session.NowPlayingItem?.RunTimeTicks) * (session.NowPlayingItem?.RunTimeTicks - remainingTicks) ); }; useEffect(() => { const currentTime = session.PlayState?.PositionTicks; const duration = session.NowPlayingItem?.RunTimeTicks; - if ( - duration !== null && - duration !== undefined && - currentTime !== null && - currentTime !== undefined - ) { + if (duration !== null && duration !== undefined && currentTime !== null && currentTime !== undefined) { const remainingTimeTicks = duration - currentTime; setRemainingTicks(remainingTimeTicks); } }, [session]); + const { data: ipInfo } = useQuery({ + queryKey: ["ipinfo", session.RemoteEndPoint], + cacheTime: Infinity, + queryFn: async () => { + const resp = await api.axiosInstance.get(`https://freeipapi.com/api/json/${session.RemoteEndPoint}`); + return resp.data; + }, + }); + useInterval(tick, 1000); return ( - + {session.NowPlayingItem?.Type === "Episode" ? ( <> - - {session.NowPlayingItem?.Name} - + {session.NowPlayingItem?.Name} {`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`} {" - "} @@ -122,15 +114,9 @@ const SessionCard = ({ session }: SessionCardProps) => { ) : ( <> - - {session.NowPlayingItem?.Name} - - - {session.NowPlayingItem?.ProductionYear} - - - {session.NowPlayingItem?.SeriesName} - + {session.NowPlayingItem?.Name} + {session.NowPlayingItem?.ProductionYear} + {session.NowPlayingItem?.SeriesName} )} @@ -140,6 +126,8 @@ const SessionCard = ({ session }: SessionCardProps) => { {session.Client} {"\n"} {session.DeviceName} + {"\n"} + {ipInfo?.cityName} {ipInfo?.countryCode} @@ -180,9 +168,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { const iconMap = { bitrate: , codec: , - videoRange: ( - - ), + videoRange: , resolution: , language: , audioChannels: , @@ -190,11 +176,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { } as const; const icon = (val: string) => { - return ( - iconMap[val as keyof typeof iconMap] ?? ( - - ) - ); + return iconMap[val as keyof typeof iconMap] ?? ; }; const formatVal = (key: string, val: any) => { @@ -251,9 +233,7 @@ const TranscodingStreamView = ({ return ( - - {title} - + {title} @@ -262,11 +242,7 @@ const TranscodingStreamView = ({ <> - + @@ -280,23 +256,17 @@ const TranscodingStreamView = ({ const TranscodingView = ({ session }: SessionCardProps) => { const videoStream = useMemo(() => { - return session.NowPlayingItem?.MediaStreams?.filter( - (s) => s.Type == "Video" - )[0]; + return session.NowPlayingItem?.MediaStreams?.filter((s) => s.Type == "Video")[0]; }, [session]); const audioStream = useMemo(() => { const index = session.PlayState?.AudioStreamIndex; - return index !== null && index !== undefined - ? session.NowPlayingItem?.MediaStreams?.[index] - : undefined; + return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; }, [session.PlayState?.AudioStreamIndex]); const subtitleStream = useMemo(() => { const index = session.PlayState?.SubtitleStreamIndex; - return index !== null && index !== undefined - ? session.NowPlayingItem?.MediaStreams?.[index] - : undefined; + return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; }, [session.PlayState?.SubtitleStreamIndex]); const isTranscoding = useMemo(() => { @@ -321,11 +291,7 @@ const TranscodingView = ({ session }: SessionCardProps) => { bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.VideoCodec, }} - isTranscoding={ - isTranscoding && !session.TranscodingInfo?.IsVideoDirect - ? true - : false - } + isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} /> { codec: session.TranscodingInfo?.AudioCodec, audioChannels: session.TranscodingInfo?.AudioChannels?.toString(), }} - isTranscoding={ - isTranscoding && !session.TranscodingInfo?.IsVideoDirect - ? true - : false - } + isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} /> {subtitleStream && ( From 7768939767b52452681c682a1a6205e52ac095c9 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:08:48 -0400 Subject: [PATCH 70/93] fix: NPE when unregistering receiver --- .../src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt index 97d1d7aa..7b4d8721 100644 --- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt +++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt @@ -354,7 +354,9 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context ) } - currentActivity.unregisterReceiver(actionReceiver) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + currentActivity.unregisterReceiver(actionReceiver) + } currentActivity.removeOnPictureInPictureModeChangedListener(pipChangeListener) VLCManager.listeners.clear() From ab5df3c9ef07eee192161d7d1eefd7ae82f40501 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Thu, 13 Mar 2025 15:57:56 +0100 Subject: [PATCH 71/93] fix: limit item titel to oneline --- components/ItemCardText.tsx | 4 ++-- utils/background-tasks.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/ItemCardText.tsx b/components/ItemCardText.tsx index dd9176b0..2a9995e1 100644 --- a/components/ItemCardText.tsx +++ b/components/ItemCardText.tsx @@ -13,7 +13,7 @@ export const ItemCardText: React.FC = ({ item }) => { {item.Type === "Episode" ? ( <> - + {item.Name} @@ -24,7 +24,7 @@ export const ItemCardText: React.FC = ({ item }) => { ) : ( <> - {item.Name} + {item.Name} {item.ProductionYear} )} diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index b066692a..6f92d83f 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -30,6 +30,7 @@ export const BACKGROUND_FETCH_TASK_SESSIONS = export async function registerBackgroundFetchAsyncSessions() { try { + console.log("Registering background fetch sessions"); BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, { minimumInterval: 1 * 60, // 1 minutes stopOnTerminate: false, // android only, From c0e9f29c04f7128d1f5ddde41de77cbdd81fcd6e Mon Sep 17 00:00:00 2001 From: sarendsen Date: Thu, 13 Mar 2025 16:41:43 +0100 Subject: [PATCH 72/93] fix: home refresh --- components/settings/HomeIndex.tsx | 6 +++--- utils/atoms/settings.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 9c0919f5..43e6e3d6 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -188,13 +188,13 @@ export const HomeIndex = () => { const invalidateCache = useInvalidatePlaybackProgressCache(); - const refetch = useCallback(async () => { + const refetch = async () => { setLoading(true); await refreshStreamyfinPluginSettings(); await invalidateCache(); setLoading(false); - }, []); - + }; + const createCollectionConfig = useCallback( ( title: string, diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 34b2f776..50046a7e 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,4 +1,4 @@ -import { atom, useAtom } from "jotai"; +import { atom, useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { storage } from "../mmkv"; @@ -226,7 +226,7 @@ export const settingsAtom = atom | null>(null); export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); export const useSettings = () => { - const [api] = useAtom(apiAtom); + const api = useAtomValue(apiAtom); const [_settings, setSettings] = useAtom(settingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); From 9f17f13175763843e94882cd42882aea50f74687 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Thu, 13 Mar 2025 17:00:24 +0100 Subject: [PATCH 73/93] refactor: remove tv version of home --- components/settings/HomeIndex.tv.tsx | 453 --------------------------- 1 file changed, 453 deletions(-) delete mode 100644 components/settings/HomeIndex.tv.tsx diff --git a/components/settings/HomeIndex.tv.tsx b/components/settings/HomeIndex.tv.tsx deleted file mode 100644 index b7a8633c..00000000 --- a/components/settings/HomeIndex.tv.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { Button } from "@/components/Button"; -import { Text } from "@/components/common/Text"; -import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; -import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; -import { Loader } from "@/components/Loader"; -import { MediaListSection } from "@/components/medialists/MediaListSection"; -import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { Ionicons } from "@expo/vector-icons"; -import { Api } from "@jellyfin/sdk"; -import { - BaseItemDto, - BaseItemKind, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { - getItemsApi, - getSuggestionsApi, - getTvShowsApi, - getUserLibraryApi, - getUserViewsApi, -} from "@jellyfin/sdk/lib/utils/api"; -import NetInfo from "@react-native-community/netinfo"; -import { QueryFunction, useQuery } from "@tanstack/react-query"; -import { useRouter } from "expo-router"; -import { useAtomValue } from "jotai"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - ActivityIndicator, - RefreshControl, - ScrollView, - View, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -type ScrollingCollectionListSection = { - type: "ScrollingCollectionList"; - title?: string; - queryKey: (string | undefined | null)[]; - queryFn: QueryFunction; - orientation?: "horizontal" | "vertical"; -}; - -type MediaListSection = { - type: "MediaListSection"; - queryKey: (string | undefined)[]; - queryFn: QueryFunction; -}; - -type Section = ScrollingCollectionListSection | MediaListSection; - -export const HomeIndex = () => { - const router = useRouter(); - - const { t } = useTranslation(); - - const api = useAtomValue(apiAtom); - const user = useAtomValue(userAtom); - - const [loading, setLoading] = useState(false); - const [ - settings, - updateSettings, - pluginSettings, - setPluginSettings, - refreshStreamyfinPluginSettings, - ] = useSettings(); - - const [isConnected, setIsConnected] = useState(null); - const [loadingRetry, setLoadingRetry] = useState(false); - - const insets = useSafeAreaInsets(); - - const checkConnection = useCallback(async () => { - setLoadingRetry(true); - const state = await NetInfo.fetch(); - setIsConnected(state.isConnected); - setLoadingRetry(false); - }, []); - - useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state) => { - if (state.isConnected == false || state.isInternetReachable === false) - setIsConnected(false); - else setIsConnected(true); - }); - - NetInfo.fetch().then((state) => { - setIsConnected(state.isConnected); - }); - - // cleanCacheDirectory().catch((e) => - // console.error("Something went wrong cleaning cache directory") - // ); - - return () => { - unsubscribe(); - }; - }, []); - - const { - data, - isError: e1, - isLoading: l1, - } = useQuery({ - queryKey: ["home", "userViews", user?.Id], - queryFn: async () => { - if (!api || !user?.Id) { - return null; - } - - const response = await getUserViewsApi(api).getUserViews({ - userId: user.Id, - }); - - return response.data.Items || null; - }, - enabled: !!api && !!user?.Id, - staleTime: 60 * 1000, - }); - - const userViews = useMemo( - () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)), - [data, settings?.hiddenLibraries] - ); - - const collections = useMemo(() => { - const allow = ["movies", "tvshows"]; - return ( - userViews?.filter( - (c) => c.CollectionType && allow.includes(c.CollectionType) - ) || [] - ); - }, [userViews]); - - const invalidateCache = useInvalidatePlaybackProgressCache(); - - const refetch = useCallback(async () => { - setLoading(true); - await refreshStreamyfinPluginSettings(); - await invalidateCache(); - setLoading(false); - }, []); - - const createCollectionConfig = useCallback( - ( - title: string, - queryKey: string[], - includeItemTypes: BaseItemKind[], - parentId: string | undefined - ): ScrollingCollectionListSection => ({ - title, - queryKey, - queryFn: async () => { - if (!api) return []; - return ( - ( - await getUserLibraryApi(api).getLatestMedia({ - userId: user?.Id, - limit: 20, - fields: ["PrimaryImageAspectRatio", "Path"], - imageTypeLimit: 1, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - includeItemTypes, - parentId, - }) - ).data || [] - ); - }, - type: "ScrollingCollectionList", - }), - [api, user?.Id] - ); - - let sections: Section[] = []; - if (!settings?.home || !settings?.home?.sections) { - sections = useMemo(() => { - if (!api || !user?.Id) return []; - - const latestMediaViews = collections.map((c) => { - const includeItemTypes: BaseItemKind[] = - c.CollectionType === "tvshows" ? ["Series"] : ["Movie"]; - const title = t("home.recently_added_in", { libraryName: c.Name }); - const queryKey = [ - "home", - "recentlyAddedIn" + c.CollectionType, - user?.Id!, - c.Id!, - ]; - return createCollectionConfig( - title || "", - queryKey, - includeItemTypes, - c.Id - ); - }); - - const ss: Section[] = [ - { - title: t("home.continue_watching"), - queryKey: ["home", "resumeItems"], - queryFn: async () => - ( - await getItemsApi(api).getResumeItems({ - userId: user.Id, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - includeItemTypes: ["Movie", "Series", "Episode"], - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - { - title: t("home.next_up"), - queryKey: ["home", "nextUp-all"], - queryFn: async () => - ( - await getTvShowsApi(api).getNextUp({ - userId: user?.Id, - fields: ["MediaSourceCount"], - limit: 20, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - enableResumable: false, - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - ...latestMediaViews, - // ...(mediaListCollections?.map( - // (ml) => - // ({ - // title: ml.Name, - // queryKey: ["home", "mediaList", ml.Id!], - // queryFn: async () => ml, - // type: "MediaListSection", - // orientation: "vertical", - // } as Section) - // ) || []), - { - title: t("home.suggested_movies"), - queryKey: ["home", "suggestedMovies", user?.Id], - queryFn: async () => - ( - await getSuggestionsApi(api).getSuggestions({ - userId: user?.Id, - limit: 10, - mediaType: ["Video"], - type: ["Movie"], - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "vertical", - }, - { - title: t("home.suggested_episodes"), - queryKey: ["home", "suggestedEpisodes", user?.Id], - queryFn: async () => { - try { - const suggestions = await getSuggestions(api, user.Id); - const nextUpPromises = suggestions.map((series) => - getNextUp(api, user.Id, series.Id) - ); - const nextUpResults = await Promise.all(nextUpPromises); - - return nextUpResults.filter((item) => item !== null) || []; - } catch (error) { - console.error("Error fetching data:", error); - return []; - } - }, - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - ]; - return ss; - }, [api, user?.Id, collections]); - } else { - sections = useMemo(() => { - if (!api || !user?.Id) return []; - const ss: Section[] = []; - - for (const key in settings.home?.sections) { - // @ts-expect-error - const section = settings.home?.sections[key]; - const id = section.title || key; - ss.push({ - title: id, - queryKey: ["home", id], - queryFn: async () => { - if (section.items) { - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - limit: section.items?.limit || 25, - recursive: true, - includeItemTypes: section.items?.includeItemTypes, - sortBy: section.items?.sortBy, - sortOrder: section.items?.sortOrder, - filters: section.items?.filters, - parentId: section.items?.parentId, - }); - return response.data.Items || []; - } else if (section.nextUp) { - const response = await getTvShowsApi(api).getNextUp({ - userId: user?.Id, - fields: ["MediaSourceCount"], - limit: section.items?.limit || 25, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - enableResumable: section.items?.enableResumable || false, - enableRewatching: section.items?.enableRewatching || false, - }); - return response.data.Items || []; - } - return []; - }, - type: "ScrollingCollectionList", - orientation: section?.orientation || "vertical", - }); - } - return ss; - }, [api, user?.Id, settings.home?.sections]); - } - - if (isConnected === false) { - return ( - - {t("home.no_internet")} - - {t("home.no_internet_message")} - - - - - - - ); - } - - if (e1) - return ( - - {t("home.oops")} - - {t("home.error_message")} - - - ); - - if (l1) - return ( - - - - ); - - return ( - - } - contentContainerStyle={{ - paddingLeft: insets.left, - paddingRight: insets.right, - paddingBottom: 16, - }} - > - - - - {sections.map((section, index) => { - if (section.type === "ScrollingCollectionList") { - return ( - - ); - } else if (section.type === "MediaListSection") { - return ( - - ); - } - return null; - })} - - - ); -}; - -// Function to get suggestions -async function getSuggestions(api: Api, userId: string | undefined) { - if (!userId) return []; - const response = await getSuggestionsApi(api).getSuggestions({ - userId, - limit: 10, - mediaType: ["Unknown"], - type: ["Series"], - }); - return response.data.Items ?? []; -} - -// Function to get the next up TV show for a series -async function getNextUp( - api: Api, - userId: string | undefined, - seriesId: string | undefined -) { - if (!userId || !seriesId) return null; - const response = await getTvShowsApi(api).getNextUp({ - userId, - seriesId, - limit: 1, - }); - return response.data.Items?.[0] ?? null; -} From 10bfa9506027d59b13cb1ba7ed6d271b78282749 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:21:24 +0100 Subject: [PATCH 74/93] fix: update textContentType for username input to oneTimeCode (#587) --- app/(auth)/(tabs)/(home)/_layout.tsx | 10 ++++---- app/login.tsx | 19 ++++++++------- components/AddToFavorites.tsx | 9 +++---- components/common/ItemImage.tsx | 11 ++++----- .../video-player/controls/AudioSlider.tsx | 14 +++++------ providers/WebSocketProvider.tsx | 24 +++++++++++-------- utils/jellyfin/session/capabilities.ts | 5 ---- 7 files changed, 46 insertions(+), 46 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 6c111607..dee024fd 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -3,7 +3,7 @@ import { Ionicons, Feather } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; import { Platform, TouchableOpacity, View } from "react-native"; import { useTranslation } from "react-i18next"; -const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null; +const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); import { useAtom } from "jotai"; import { userAtom } from "@/providers/JellyfinProvider"; import { useSessions, useSessionsProps } from "@/hooks/useSessions"; @@ -25,7 +25,7 @@ export default function IndexLayout() { headerLargeStyle: { backgroundColor: "black", }, - headerTransparent: Platform.OS === "ios" ? true : false, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, headerRight: () => ( @@ -113,7 +113,7 @@ export default function IndexLayout() { title: "", headerShown: true, headerBlurEffect: "prominent", - headerTransparent: Platform.OS === "ios" ? true : false, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, }} /> @@ -137,7 +137,7 @@ const SettingsButton = () => { const SessionsButton = () => { const router = useRouter(); - const { sessions = [], _ } = useSessions({} as useSessionsProps); + const { sessions = [] } = useSessions({} as useSessionsProps); return ( { }} > - { useEffect(() => { (async () => { if (_apiUrl) { - setServer({ + await setServer({ address: _apiUrl, }); @@ -181,7 +181,7 @@ const Login: React.FC = () => { return; } - setServer({ address: url }); + await setServer({ address: url }); }, []); const handleQuickConnect = async () => { @@ -237,11 +237,12 @@ const Login: React.FC = () => { setCredentials({ ...credentials, username: text }) } value={credentials.username} - secureTextEntry={false} keyboardType="default" returnKeyType="done" autoCapitalize="none" - textContentType="username" + // Changed from username to oneTimeCode because it is a known issue in RN + // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 + textContentType="oneTimeCode" clearButtonMode="while-editing" maxLength={500} /> @@ -324,17 +325,17 @@ const Login: React.FC = () => { {t("server.connect_button")} { + onServerSelect={async (server) => { setServerURL(server.address); if (server.serverName) { setServerName(server.serverName); } - handleConnect(server.address); + await handleConnect(server.address); }} /> { - handleConnect(s.address); + onServerSelect={async (s) => { + await handleConnect(s.address); }} /> diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx index 945e9988..b190dced 100644 --- a/components/AddToFavorites.tsx +++ b/components/AddToFavorites.tsx @@ -1,16 +1,17 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useFavorite } from "@/hooks/useFavorite"; -import { View } from "react-native"; +import {View, ViewProps} from "react-native"; import { RoundButton } from "@/components/RoundButton"; +import {FC} from "react"; interface Props extends ViewProps { item: BaseItemDto; } -export const AddToFavorites = ({ item, ...props }) => { - const { isFavorite, toggleFavorite, _} = useFavorite(item); - +export const AddToFavorites:FC = ({ item, ...props }) => { + const { isFavorite, toggleFavorite } = useFavorite(item); + return ( void; } -export const ItemImage: React.FC = ({ +export const ItemImage: FC = ({ item, variant = "Primary", quality = 90, @@ -53,7 +52,7 @@ export const ItemImage: React.FC = ({ if (!source?.uri) return ( void; } const AudioSlider: React.FC = ({ setVisibility }) => { - if (Platform.isTV) return; + if (Platform.isTV) { + return; + } const volume = useSharedValue(50); // Explicitly type as number const min = useSharedValue(0); // Explicitly type as number const max = useSharedValue(100); // Explicitly type as number - const timeoutRef = useRef(null); // Use a ref to store the timeout ID + const timeoutRef = useRef(null); // Use a ref to store the timeout ID useEffect(() => { const fetchInitialVolume = async () => { @@ -50,7 +50,7 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }; useEffect(() => { - const volumeListener = VolumeManager.addVolumeListener((result) => { + const volumeListener = VolumeManager.addVolumeListener((result: VolumeResult) => { volume.value = result.volume * 100; setVisibility(true); diff --git a/providers/WebSocketProvider.tsx b/providers/WebSocketProvider.tsx index 1311459f..b61c1967 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -7,16 +7,13 @@ import React, { useMemo, useCallback, } from "react"; -import { Alert, AppState, AppStateStatus } from "react-native"; +import { AppState, AppStateStatus } from "react-native"; import { useAtomValue } from "jotai"; -import { useQuery } from "@tanstack/react-query"; import { apiAtom, getOrSetDeviceId, - userAtom, } from "@/providers/JellyfinProvider"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; -import native from "@/utils/profiles/native"; interface WebSocketProviderProps { children: ReactNode; @@ -30,7 +27,6 @@ interface WebSocketContextType { const WebSocketContext = createContext(null); export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { - const user = useAtomValue(userAtom); const api = useAtomValue(apiAtom); const [ws, setWs] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -40,7 +36,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }, []); const connectWebSocket = useCallback(() => { - if (!deviceId || !api?.accessToken) return; + if (!deviceId || !api?.accessToken) { + return; + } const protocol = api.basePath.includes("https") ? "wss" : "ws"; const url = `${protocol}://${api.basePath @@ -50,7 +48,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }&deviceId=${deviceId}`; const newWebSocket = new WebSocket(url); - let keepAliveInterval: NodeJS.Timeout | null = null; + let keepAliveInterval: number | null = null; newWebSocket.onopen = () => { setIsConnected(true); @@ -67,14 +65,18 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }; newWebSocket.onclose = () => { - if (keepAliveInterval) clearInterval(keepAliveInterval); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } setIsConnected(false); }; setWs(newWebSocket); return () => { - if (keepAliveInterval) clearInterval(keepAliveInterval); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } newWebSocket.close(); }; }, [api, deviceId]); @@ -85,7 +87,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }, [connectWebSocket]); useEffect(() => { - if (!deviceId || !api || !api?.accessToken) return; + if (!deviceId || !api || !api?.accessToken) { + return; + } const init = async () => { await getSessionApi(api).postFullCapabilities({ diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index 99ef5cb1..c0f3b295 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,12 +1,8 @@ import { Settings } from "@/utils/atoms/settings"; -import ios from "@/utils/profiles/ios"; import native from "@/utils/profiles/native"; -import old from "@/utils/profiles/old"; import { Api } from "@jellyfin/sdk"; import { AxiosError, AxiosResponse } from "axios"; -import { useMemo } from "react"; import { getAuthHeaders } from "../jellyfin"; -import iosFmp4 from "@/utils/profiles/iosFmp4"; interface PostCapabilitiesParams { api: Api | null | undefined; @@ -25,7 +21,6 @@ export const postCapabilities = async ({ api, itemId, sessionId, - deviceProfile, }: PostCapabilitiesParams): Promise => { if (!api || !itemId || !sessionId) { throw new Error("Missing parameters for marking item as not played"); From cbcb160bdd94cc10be7c1451547da5d7b45ce390 Mon Sep 17 00:00:00 2001 From: Danylo Kozhushko <32894068+ozgreat@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:21:52 +0100 Subject: [PATCH 75/93] feat: Added Ukrainian translation (#593) --- i18n.ts | 3 + translations/ua.json | 475 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 478 insertions(+) create mode 100644 translations/ua.json diff --git a/i18n.ts b/i18n.ts index 2480e384..7f35e575 100644 --- a/i18n.ts +++ b/i18n.ts @@ -10,6 +10,7 @@ import ja from "./translations/ja.json"; import tr from "./translations/tr.json"; import nl from "./translations/nl.json"; import sv from "./translations/sv.json"; +import ua from "./translations/ua.json" import zhCN from './translations/zh-CN.json'; import zhTW from './translations/zh-TW.json'; import { getLocales } from "expo-localization"; @@ -24,6 +25,7 @@ export const APP_LANGUAGES = [ { label: "Türkçe", value: "tr" }, { label: "Nederlands", value: "nl" }, { label: "Svenska", value: "sv" }, + { label: "Українська", value: "ua" }, { label: "简体中文", value: "zh-CN" }, { label: "繁體中文", value: "zh-TW" }, ]; @@ -40,6 +42,7 @@ i18n.use(initReactI18next).init({ nl: { translation: nl }, sv: { translation: sv }, tr: { translation: tr }, + ua: { translation: ua }, "zh-CN": { translation: zhCN }, "zh-TW": { translation: zhTW }, }, diff --git a/translations/ua.json b/translations/ua.json new file mode 100644 index 00000000..d2a40d71 --- /dev/null +++ b/translations/ua.json @@ -0,0 +1,475 @@ +{ + "login": { + "username_required": "Імʼя користувача необхідне", + "error_title": "Помилка", + "login_title": "Вхід", + "login_to_title": "Увійти в", + "username_placeholder": "Імʼя користувача", + "password_placeholder": "Пароль", + "login_button": "Вхід", + "quick_connect": "Швидке Зʼєднання", + "enter_code_to_login": "Введіть код {{code}} для входу", + "failed_to_initiate_quick_connect": "Не вдалося ініціалізувати Швидке Зʼєднання", + "got_it": "Готово", + "connection_failed": "Помилка зʼєднання", + "could_not_connect_to_server": "Неможливо підʼєднатися до серверу. Будь ласка перевірте URL і ваше зʼєднання з мережею", + "an_unexpected_error_occured": "Сталася несподівана помилка", + "change_server": "Змінити сервер", + "invalid_username_or_password": "Неправильні імʼя користувача або пароль", + "user_does_not_have_permission_to_log_in": "Користувач не маю дозволу на вхід", + "server_is_taking_too_long_to_respond_try_again_later": "Сервер відповідає занадто довго, будь-ласка спробуйте пізніше", + "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", + "there_is_a_server_error": "Відбулася помилка на стороні сервера", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?" + }, + "server": { + "enter_url_to_jellyfin_server": "Введіть URL вашого Jellyfin сервера", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "Підʼєднатися", + "previous_servers": "попередні сервери", + "clear_button": "Очистити", + "search_for_local_servers": "Пошук локальних серверів", + "searching": "Пошук...", + "servers": "Сервери" + }, + "home": { + "no_internet": "Інтернет відсутній", + "no_items": "Пусто", + "no_internet_message": "Не хвилюйтеся, ви все ще можете переглядати\nзавантажений контент.", + "go_to_downloads": "Перейти в завантаження", + "oops": "Упс!", + "error_message": "Щось пішло не так.\nБудь ласка вийдіть і увійдіть знов.", + "continue_watching": "Продовжити перегляд", + "next_up": "Далі", + "recently_added_in": "Нещодавно додане до медіатеки {{libraryName}}", + "suggested_movies": "Рекомендовані Фільми", + "suggested_episodes": "Рекомендовані Епізоди", + "intro": { + "welcome_to_streamyfin": "Вітаємо у Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Вільний і open-source клієнт для Jellyfin.", + "features_title": "Функції", + "features_description": "Streamyfin має безліч функцій та інтегрується з широким спектром програмного забезпечення, яке ви можете знайти в меню налаштувань, зокрема:", + "jellyseerr_feature_description": "Підключіться до вашого екземпляру Jellyseerr і запитуватуйте фільми безпосередньо в застосунку.", + "downloads_feature_title": "Завантаження", + "downloads_feature_description": "Завантажуйте фільми і серіали для перегляду офлайн. Використовуйте або метод за замовчуванням або встановіть оптимізований сервер для завантаження файлів у фоні.", + "chromecast_feature_description": "Транслюйте фільми і серіали но ваші Chromecast прилади.", + "centralised_settings_plugin_title": "Centralised Settings Plugin", + "centralised_settings_plugin_description": "Налаштуйте параметри з централізованої локації на вашому сервері Jellyfin. Всі налаштування клієнтів для всіх користувачів будуть синхронізовані автоматично.", + "done_button": "Готово", + "go_to_settings_button": "Перейти до параметрів", + "read_more": "Прочитати більше" + }, + "settings": { + "settings_title": "Параметри", + "log_out_button": "Вихід", + "user_info": { + "user_info_title": "Інформація користувача", + "user": "Користувач", + "server": "Сервер", + "token": "Токен", + "app_version": "Версія Застосунку" + }, + "quick_connect": { + "quick_connect_title": "Швидке Зʼєднання", + "authorize_button": "Авторизуйте Швидке Зʼєднання", + "enter_the_quick_connect_code": "Введіть код для швидкого зʼєднання...", + "success": "Успіх", + "quick_connect_autorized": "Швидке Зʼєднання авторизовано", + "error": "Помилка", + "invalid_code": "Не правильний код", + "authorize": "Авторизувати" + }, + "media_controls": { + "media_controls_title": "Керування Медія", + "forward_skip_length": "Тривалість перемотування вперед", + "rewind_length": "Довжина перемотування назад", + "seconds_unit": "с" + }, + "audio": { + "audio_title": "Аудіо", + "set_audio_track": "Виставити аудіо доріжку як в попередньому епізоду", + "audio_language": "Мова аудіо", + "audio_hint": "Вибрати мову аудіо за замовчуванням.", + "none": "Ніяка", + "language": "Мова" + }, + "subtitles": { + "subtitle_title": "Субтитри", + "subtitle_language": "Мова субтитрів", + "subtitle_mode": "Режим субтитрів", + "set_subtitle_track": "Виставити доріжку субтитрів як в попередньому епізоду", + "subtitle_size": "Розмір субтитрів", + "subtitle_hint": "Налаштуйте параметри субтитрів.", + "none": "Ніякі", + "language": "Мова", + "loading": "Завантаження", + "modes": { + "Default": "За замовчування", + "Smart": "Smart", + "Always": "Завжди", + "None": "Някий", + "OnlyForced": "Виключно Форсовані" + } + }, + "other": { + "other_title": "Інші", + "follow_device_orientation": "Дотримуйтесь орієнтації пристрою", + "video_orientation": "Орієнтація відео", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "За змовчуванням", + "ALL": "Всі", + "PORTRAIT": "Портретна", + "PORTRAIT_UP": "Портретна Догори", + "PORTRAIT_DOWN": "Портретна Донизу", + "LANDSCAPE": "Альбомна", + "LANDSCAPE_LEFT": "Альбомна Ліва", + "LANDSCAPE_RIGHT": "Альбомна Права", + "OTHER": "Інше", + "UNKNOWN": "Невідомо" + }, + "safe_area_in_controls": "Безпечна зона в елементах керування", + "video_player": "Відео плеєр", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Показати посилання на користувацьке меню", + "hide_libraries": "Сховати медіатеки", + "select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.", + "disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок", + "default_quality": "Якість за замовченням" + }, + "downloads": { + "downloads_title": "Завантаження", + "download_method": "Метод завантаження", + "remux_max_download": "Remux max download", + "auto_download": "Авто-завантаження", + "optimized_versions_server": "Optimized versions server", + "save_button": "Зберегти", + "optimized_server": "Оптимізований Сервер", + "optimized": "Оптимізований", + "default": "За замовченням", + "optimized_version_hint": "Введіть URL-адресу сервера для оптимізації. URL-адреса має містити http або https і, за бажанням, порт.", + "read_more_about_optimized_server": "Дізнайтеся більше про сервер для оптимізації.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Плагіни", + "jellyseerr": { + "jellyseerr_warning": "Ця інтеграція перебуває на початковій стадії. Очікуйте, що все зміниться.", + "server_url": "URL Сервера", + "server_url_hint": "Наприклад: http(s)://your-host.url\n(додайте порт якщо необхідно)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Пароль", + "password_placeholder": "Введіть Jellyfin пароль для користувача {{username}}", + "save_button": "Зберегти", + "clear_button": "Очистити", + "login_button": "Вхід", + "total_media_requests": "Загальна кількість медіа запитів", + "movie_quota_limit": "Дні квоти на фільми", + "movie_quota_days": "Дні квоти на фільми", + "tv_quota_limit": "Дні квоти на серіали", + "tv_quota_days": "Дні квоти на серіали", + "reset_jellyseerr_config_button": "Скинути конфігурацію Jellyseerr", + "unlimited": "Необмежене", + "plus_n_more": "+{{n}} ще", + "order_by": { + "DEFAULT": "За замовченням", + "VOTE_COUNT_AND_AVERAGE": "Кількість голосів і середнє", + "POPULARITY": "Популярність" + } + }, + "marlin_search": { + "enable_marlin_search": "Увімкнути Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Введіть URL-адресу сервера Marlin. Адреса повинна містити http або https і, за бажанням, порт.", + "read_more_about_marlin": "Дізнайтеся більше про Marlin.", + "save_button": "Зберегти", + "toasts": { + "saved": "Збережено" + } + } + }, + "storage": { + "storage_title": "Сховище", + "app_usage": "Застосунок {{usedSpace}}%", + "device_usage": "Гаджет {{availableSpace}}%", + "size_used": "{{used}} з {{total}} використано", + "delete_all_downloaded_files": "Видалити усі завантаженні файли" + }, + "intro": { + "show_intro": "Показати інтро", + "reset_intro": "Скинути інтро" + }, + "logs": { + "logs_title": "Журнал", + "no_logs_available": "Нема доступних журналів", + "delete_all_logs": "Видалити усі журнали" + }, + "languages": { + "title": "Мова", + "app_language": "Мова застосунку", + "app_language_description": "Виберіть мову застосунку.", + "system": "Системна" + }, + "toasts": { + "error_deleting_files": "Помилка при видалені файлів", + "background_downloads_enabled": "Завантаження в фоні увімкнене", + "background_downloads_disabled": "Завантаження в фоні вимкнене", + "connected": "Зʼєднано", + "could_not_connect": "Неможливо зʼєднатися", + "invalid_url": "Неправльий URL" + } + }, + "sessions": { + "title": "Сесії", + "no_active_sessions": "Нема активних сесій" + }, + "downloads": { + "downloads_title": "Завантаження", + "tvseries": "ТБ-Серіали", + "movies": "Фільми", + "queue": "Черга", + "queue_hint": "Черга і завантаження буде втрачене при перезапуску застосунку", + "no_items_in_queue": "Нема елементів в черзі", + "no_downloaded_items": "Нема завантажених елементів", + "delete_all_movies_button": "Видалити всі Фільми", + "delete_all_tvseries_button": "Видалити всі ТБ-Серіали", + "delete_all_button": "Видалити Все", + "active_download": "Активне завантаження", + "no_active_downloads": "Нема активних завантажень", + "active_downloads": "Активні завантаження", + "new_app_version_requires_re_download": "Нова версія застосунку вимагає завантажити заново", + "new_app_version_requires_re_download_description": "Нове оновлення вимагає повторного завантаження вмісту. Будь ласка, видаліть весь завантажений вміст і повторіть спробу.", + "back": "Назад", + "delete": "Видалити", + "something_went_wrong": "Щось пішло не так", + "could_not_get_stream_url_from_jellyfin": "Не вдалося отримати URL-адресу потоку від Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Методи", + "toasts": { + "you_are_not_allowed_to_download_files": "Вам не дозволено завантажувати файли.", + "deleted_all_movies_successfully": "Видалення всіх фільмів було успішне!", + "failed_to_delete_all_movies": "Не вдалося видалити усі фільми", + "deleted_all_tvseries_successfully": "Успішно видалено всі серіали!", + "failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали", + "download_cancelled": "Завантаження скасоване", + "could_not_cancel_download": "Неможливо скасувати завантаження", + "download_completed": "Завантаження завершено", + "download_started_for": "Почалося завантаження {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} вже завантажено", + "download_stated_for_item": "Почалося завантаження {{item}}", + "download_failed_for_item": "Не вдалося завантажити {{item}} - {{error}}", + "download_completed_for_item": "Завантаження завершено {{item}}", + "queued_item_for_optimization": "{{item}} в черзі на оптимізацію", + "failed_to_start_download_for_item": "Не вдалося почати завантаження {{item}}: {{message}}", + "server_responded_with_status_code": "Сервер відповів зі статусом {{statusCode}}", + "no_response_received_from_server": "Не отримано відповіді від сервера", + "error_setting_up_the_request": "Помилка налаштування запиту", + "failed_to_start_download_for_item_unexpected_error": "Не вдалося почати завантаження {{item}}: Несподівана помилка", + "all_files_folders_and_jobs_deleted_successfully": "Усі файли, папки та завдання успішно видалено", + "an_error_occured_while_deleting_files_and_jobs": "Виникла помилка під час видалення файлів і завдань", + "go_to_downloads": "Перейти до завантаження" + } + } + }, + "search": { + "search_here": "Шукати тут...", + "search": "Шукати...", + "x_items": "{{count}} елементів", + "library": "Медіатека", + "discover": "Відкрийте для себе", + "no_results": "Без результатів", + "no_results_found_for": "Жодних результатів не знайдено для", + "movies": "Фільми", + "series": "Серіали", + "episodes": "Епізоди", + "collections": "Колекції", + "actors": "Актори", + "request_movies": "Запитати Фільми", + "request_series": "Запитати Серіали", + "recently_added": "Нещодавно Додане", + "recent_requests": "Нещодавні Запити", + "plex_watchlist": "Список перегляду Plex", + "trending": "У Тренді", + "popular_movies": "Популярні Фільми", + "movie_genres": "Жанри Кіно", + "upcoming_movies": "Майбутні Фільми", + "studios": "Студії", + "popular_tv": "Популярні Серіали", + "tv_genres": "Жанри Серіалів", + "upcoming_tv": "Майбутні Серіали", + "networks": "ТБ Канали", + "tmdb_movie_keyword": "TMDB Ключові слова Фільмів", + "tmdb_movie_genre": "TMDB Жанри Кіно", + "tmdb_tv_keyword": "TMDB ТБ Ключові слова", + "tmdb_tv_genre": "TMDB ТБ Жанри", + "tmdb_search": "TMDB Пошук", + "tmdb_studio": "TMDB Студії", + "tmdb_network": "TMDB ТБ Канали", + "tmdb_movie_streaming_services": "TMDB Стрімінгові Сервіси Фільмів", + "tmdb_tv_streaming_services": "TMDB Стрімінгові Сервіси Серіалів" + }, + "library": { + "no_items_found": "Елементів не знайдено", + "no_results": "Без результатів", + "no_libraries_found": "Не знайдено медіатек", + "item_types": { + "movies": "фільми", + "series": "серіали", + "boxsets": "бокс-сети", + "items": "елементи" + }, + "options": { + "display": "Показати", + "row": "Ряд", + "list": "Список", + "image_style": "Стиль зображення", + "poster": "Постер", + "cover": "Обкладинка", + "show_titles": "Показати заголовки", + "show_stats": "Показати статистику" + }, + "filters": { + "genres": "Жанри", + "years": "Роки", + "sort_by": "Відсортувати за", + "sort_order": "Порядок сортування", + "asc": "За зростанням", + "desc": "За спаданням", + "tags": "Теги" + } + }, + "favorites": { + "series": "Серіали", + "movies": "Фільми", + "episodes": "Епізоди", + "videos": "Відео", + "boxsets": "Бокс-сети", + "playlists": "Плейлісти" + }, + "custom_links": { + "no_links": "Немає посилань" + }, + "player": { + "error": "Помилка", + "failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку", + "an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнали в налаштуваннях.", + "client_error": "Помилка клієнту", + "could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast", + "message_from_server": "Повідомлення від серверу: {{message}}", + "video_has_finished_playing": "Відтворення відео завершено!", + "no_video_source": "Немає джерела відео...", + "next_episode": "Наступний Епізод", + "refresh_tracks": "Оновити доріжки", + "subtitle_tracks": "Доріжки Субтитрів:", + "audio_tracks": "Аудіо-доріжки:", + "playback_state": "Стан відтворення:", + "no_data_available": "Дані відсутні", + "index": "Індекс:" + }, + "item_card": { + "next_up": "Далі", + "no_items_to_display": "Немає елементів для відображення", + "cast_and_crew": "Акторський склад та команда", + "series": "Серіали", + "seasons": "Сезони", + "season": "Сезон", + "no_episodes_for_this_season": "У цьому сезоні немає епізодів", + "overview": "Огляд", + "more_with": "Більше з {{name}}", + "similar_items": "Схожі елементи", + "no_similar_items_found": "Не знайдено схожих елементів", + "video": "Відео", + "more_details": "Більше деталей", + "quality": "Якість", + "audio": "Аудіо", + "subtitles": "Субтитри", + "show_more": "Показати більше", + "show_less": "Показати менше", + "appeared_in": "Зʼявлявся у", + "could_not_load_item": "Неможливо завантажити елемент", + "none": "Нічого", + "download": { + "download_season": "Завантажити Сезон", + "download_series": "Завантажити Серіал", + "download_episode": "Завантажити Епізод", + "download_movie": "Завантажити Фільм", + "download_x_item": "Завантажено {{item_count}} елементів", + "download_button": "Завантажити", + "using_optimized_server": "Використовуючи сервер оптимізації", + "using_default_method": "Використовуючи метод за замовченням" + } + }, + "live_tv": { + "next": "Наступний", + "previous": "Попередній", + "live_tv": "Live TV", + "coming_soon": "Скоро", + "on_now": "Просто зараз", + "shows": "Серіали", + "movies": "Фільми", + "sports": "Спорт", + "for_kids": "Для дітей", + "news": "Новини" + }, + "jellyseerr": { + "confirm": "Підтвердити", + "cancel": "Скасувати", + "yes": "Так", + "whats_wrong": "Щось сталося?", + "issue_type": "Тип проблеми", + "select_an_issue": "Виберіть проблему", + "types": "Типи", + "describe_the_issue": "(опціонально) Опишіть проблему...", + "submit_button": "Надіслати", + "report_issue_button": "Звіт про проблему", + "request_button": "Запити", + "are_you_sure_you_want_to_request_all_seasons": "Ви впевнені, що хочете запросити всі сезони?", + "failed_to_login": "Не вдалося увійти", + "cast": "Акторський склад", + "details": "Деталі", + "status": "Статус", + "original_title": "Оригінальна Назва", + "series_type": "Тип Серіалу", + "release_dates": "Дата Виходу", + "first_air_date": "Дата першого етеру", + "next_air_date": "Дата наступного етеру", + "revenue": "Збори", + "budget": "Бюджет", + "original_language": "Мова Оригіналу", + "production_country": "Країна Виробництва", + "studios": "Студії", + "network": "ТБ Канали", + "currently_streaming_on": "Наразі транслюється на", + "advanced": "Просунуте", + "request_as": "Запит Як", + "tags": "Теги", + "quality_profile": "Профіль якості", + "root_folder": "Корнева Тека", + "season_all": "Сезон (всі)", + "season_number": "Сезон {{season_number}}", + "number_episodes": "{{episode_number}} Епізодів", + "born": "Дата народження", + "appearances": "Зовнішній вигляд", + "toasts": { + "jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не відповідає мінімальним вимогам до версії! Будь ласка, оновіть версію принаймні до 2.0.0", + "jellyseerr_test_failed": "Тест Jellyseerr завершився невдало. Спробуйте ще раз.", + "failed_to_test_jellyseerr_server_url": "Не вдалося перевірити URL-адресу сервера jellyseerr", + "issue_submitted": "Звіт про проблему відправлено", + "requested_item": "Запитано {{item}}!", + "you_dont_have_permission_to_request": "У вас нема дозволу на запит медіа!", + "something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!" + } + }, + "tabs": { + "home": "Головна", + "search": "Пошук", + "library": "Медіатека", + "custom_links": "Ваші Посилання", + "favorites": "Улюблене" + } +} From 9b0ba285b39a20023b01a17e185534a91adc1876 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 10 Mar 2025 01:05:51 -0400 Subject: [PATCH 76/93] feat: Ability to consume webhook notifications and forward to clients #595 - forward expo device tokens to users plugin instance - added android notification icon --- app.json | 9 ++++- app/_layout.tsx | 65 +++++++++++++++++++++++++++++---- assets/images/notification.png | Bin 0 -> 22765 bytes augmentations/api.ts | 17 ++++++++- providers/JellyfinProvider.tsx | 5 +++ 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 assets/images/notification.png diff --git a/app.json b/app.json index 8867d0cc..ba4584d5 100644 --- a/app.json +++ b/app.json @@ -120,6 +120,13 @@ "image": "./assets/images/StreamyFinFinal.png", "imageWidth": 100 } + ], + [ + "expo-notifications", + { + "icon": "./assets/images/notification.png", + "color": "#9333EA" + } ] ], "experiments": { @@ -133,7 +140,7 @@ "projectId": "e79219d1-797f-4fbe-9fa1-cfd360690a68" } }, - "owner": "fredrikburmester", + "owner": "streamyfin", "runtimeVersion": { "policy": "appVersion" }, diff --git a/app/_layout.tsx b/app/_layout.tsx index 8dfe0786..2d1ab164 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -12,7 +12,7 @@ import { BACKGROUND_FETCH_TASK_SESSIONS, registerBackgroundFetchAsyncSessions, } from "@/utils/background-tasks"; -import { LogProvider, writeToLog } from "@/utils/log"; +import {LogProvider, writeErrorLog, writeToLog} from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; @@ -30,7 +30,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; import { getLocales } from "expo-localization"; import { Provider as JotaiProvider } from "jotai"; -import { useEffect, useRef } from "react"; +import {useEffect, useRef, useState} from "react"; import { I18nextProvider } from "react-i18next"; import { Appearance, AppState } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; @@ -41,6 +41,9 @@ import { useAtom } from "jotai"; import { userAtom } from "@/providers/JellyfinProvider"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { store } from "@/utils/store"; +import {EventSubscription} from "expo-modules-core"; +import {ExpoPushToken} from "expo-notifications/build/Tokens.types"; +import {Notification, NotificationResponse} from "expo-notifications/build/Notifications.types"; if (!Platform.isTV) { Notifications.setNotificationHandler({ @@ -258,6 +261,7 @@ const queryClient = new QueryClient({ function Layout() { const [settings] = useSettings(); const [user] = useAtom(userAtom); + const [api] = useAtom(apiAtom); const appState = useRef(AppState.currentState); const segments = useSegments(); @@ -268,13 +272,58 @@ function Layout() { if (!Platform.isTV) { useNotificationObserver(); + const [expoPushToken, setExpoPushToken] = useState(); + const notificationListener = useRef(); + const responseListener = useRef(); + useEffect(() => { - checkAndRequestPermissions(); - (async () => { - if (!Platform.isTV && user && user.Policy?.IsAdministrator) { - registerBackgroundFetchAsyncSessions(); - } - })(); + if (expoPushToken && api && user) { + api?.post("/Streamyfin/device", { + token: expoPushToken.data, + deviceId: getOrSetDeviceId(), + userId: user.Id + }).then(_ => console.log("Posted expo push token")) + .catch(_ => writeErrorLog("Failed to push expo push token to plugin")) + } + else console.log("No token available") + }, [api, expoPushToken, user]); + + async function registerNotifications() { + if (Platform.OS === 'android') { + console.log("Setting android notification channel 'default'") + await Notifications?.setNotificationChannelAsync('default', { + name: 'default' + }); + } + + await checkAndRequestPermissions(); + + if (!Platform.isTV && user && user.Policy?.IsAdministrator) { + await registerBackgroundFetchAsyncSessions(); + } + + Notifications?.getExpoPushTokenAsync() + .then((token: ExpoPushToken) => token && setExpoPushToken(token)) + .catch((reason: any) => console.log("Failed to get token", reason)); + } + + useEffect(() => { + registerNotifications() + + notificationListener.current = Notifications?.addNotificationReceivedListener((notification: Notification) => { + console.log("Notification received while app running", notification); + }); + + responseListener.current = Notifications?.addNotificationResponseReceivedListener((response: NotificationResponse) => { + console.log("Notification interacted with", response); + }); + + return () => { + notificationListener.current && + Notifications?.removeNotificationSubscription(notificationListener.current); + responseListener.current && + Notifications?.removeNotificationSubscription(responseListener.current); + } }, []); useEffect(() => { diff --git a/assets/images/notification.png b/assets/images/notification.png new file mode 100644 index 0000000000000000000000000000000000000000..b50e56aed4af6f931f3cacc7b9be59276c83cf41 GIT binary patch literal 22765 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 ): Promise>; + delete( + url: string, + config?: AxiosRequestConfig + ): Promise>; getStreamyfinPluginConfig(): Promise>; } } @@ -32,9 +36,18 @@ Api.prototype.post = function ( data: D, config: AxiosRequestConfig ): Promise> { - return this.axiosInstance.post(`${this.basePath}${url}`, { + return this.axiosInstance.post(`${this.basePath}${url}`, data, { + ...(config || {}), + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, + }); +}; + +Api.prototype.delete = function ( + url: string, + config: AxiosRequestConfig +): Promise> { + return this.axiosInstance.delete(`${this.basePath}${url}`, { ...(config || {}), - data, headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, }); }; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 37b52346..1256d2ae 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -25,6 +25,7 @@ import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { getDeviceName } from "react-native-device-info"; import uuid from "react-native-uuid"; +import {writeErrorLog, writeInfoLog} from "@/utils/log"; interface Server { address: string; @@ -286,6 +287,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const logoutMutation = useMutation({ mutationFn: async () => { + api?.delete(`/Streamyfin/device/${deviceId}`) + .then(r => writeInfoLog("Deleted expo push token for device")) + .catch(e => writeErrorLog(`Failed to delete expo push token for device`)) + storage.delete("token"); setUser(null); setApi(null); From 6de829c16d93ab3031a8f4539094f2dbc56a2a0b Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 15 Mar 2025 19:17:13 +0100 Subject: [PATCH 77/93] fix: owner of app moved to org --- app.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app.json b/app.json index 8867d0cc..f93c0009 100644 --- a/app.json +++ b/app.json @@ -35,7 +35,6 @@ "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive_icon.png", "backgroundColor": "#464646" - }, "package": "com.fredrikburmester.streamyfin", "permissions": [ @@ -133,7 +132,7 @@ "projectId": "e79219d1-797f-4fbe-9fa1-cfd360690a68" } }, - "owner": "fredrikburmester", + "owner": "streamyfin", "runtimeVersion": { "policy": "appVersion" }, From cd42a86d4019937def468f7f633d766305817c38 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:19:12 +0100 Subject: [PATCH 78/93] feat: enhance favorites with empty cell && added translations (#594) --- components/home/Favorites.tsx | 248 +++++---- translations/de.json | 940 ++++++++++++++++----------------- translations/en.json | 948 +++++++++++++++++----------------- translations/es.json | 940 ++++++++++++++++----------------- translations/fr.json | 940 ++++++++++++++++----------------- translations/it.json | 940 ++++++++++++++++----------------- translations/ja.json | 938 ++++++++++++++++----------------- translations/nl.json | 940 ++++++++++++++++----------------- translations/sv.json | 60 ++- translations/tr.json | 938 ++++++++++++++++----------------- translations/zh-CN.json | 938 ++++++++++++++++----------------- translations/zh-TW.json | 938 ++++++++++++++++----------------- types.d.ts | 9 + 13 files changed, 4908 insertions(+), 4809 deletions(-) create mode 100644 types.d.ts diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index c4ab373e..fba0ca6c 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -1,100 +1,166 @@ +import { Colors } from "@/constants/Colors"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useAtom } from "jotai"; -import { View } from "react-native"; -import { ScrollingCollectionList } from "./ScrollingCollectionList"; -import { useCallback } from "react"; -import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import { t } from "i18next"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; +import { Image, Text, View } from "react-native"; +import { ScrollingCollectionList } from "./ScrollingCollectionList"; + +// PNG ASSET +import heart from "@/assets/icons/heart.fill.png"; + +type FavoriteTypes = + | "Series" + | "Movie" + | "Episode" + | "Video" + | "BoxSet" + | "Playlist"; +type EmptyState = Record; export const Favorites = () => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const [emptyState, setEmptyState] = useState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); - const fetchFavoritesByType = useCallback( - async (itemType: BaseItemKind) => { - const response = await getItemsApi(api!).getItems({ - userId: user?.Id!, - sortBy: ["SeriesSortName", "SortName"], - sortOrder: ["Ascending"], - filters: ["IsFavorite"], - recursive: true, - fields: ["PrimaryImageAspectRatio"], - collapseBoxSetItems: false, - excludeLocationTypes: ["Virtual"], - enableTotalRecordCount: false, - limit: 20, - includeItemTypes: [itemType], - }); - return response.data.Items || []; - }, - [api, user] - ); + const fetchFavoritesByType = useCallback( + async (itemType: BaseItemKind) => { + const response = await getItemsApi(api as Api).getItems({ + userId: user?.Id, + sortBy: ["SeriesSortName", "SortName"], + sortOrder: ["Ascending"], + filters: ["IsFavorite"], + recursive: true, + fields: ["PrimaryImageAspectRatio"], + collapseBoxSetItems: false, + excludeLocationTypes: ["Virtual"], + enableTotalRecordCount: false, + limit: 20, + includeItemTypes: [itemType], + }); + const items = response.data.Items || []; - const fetchFavoriteSeries = useCallback( - () => fetchFavoritesByType("Series"), - [fetchFavoritesByType] - ); - const fetchFavoriteMovies = useCallback( - () => fetchFavoritesByType("Movie"), - [fetchFavoritesByType] - ); - const fetchFavoriteEpisodes = useCallback( - () => fetchFavoritesByType("Episode"), - [fetchFavoritesByType] - ); - const fetchFavoriteVideos = useCallback( - () => fetchFavoritesByType("Video"), - [fetchFavoritesByType] - ); - const fetchFavoriteBoxsets = useCallback( - () => fetchFavoritesByType("BoxSet"), - [fetchFavoritesByType] - ); - const fetchFavoritePlaylists = useCallback( - () => fetchFavoritesByType("Playlist"), - [fetchFavoritesByType] - ); + // Update empty state for this specific type + setEmptyState((prev) => ({ + ...prev, + [itemType as FavoriteTypes]: items.length === 0, + })); - return ( - - - - - - - - - ); + return items; + }, + [api, user], + ); + + // Reset empty state when component mounts or dependencies change + useEffect(() => { + setEmptyState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); + }, [api, user]); + + // Check if all categories that have been loaded are empty + const areAllEmpty = () => { + const loadedCategories = Object.values(emptyState); + return ( + loadedCategories.length > 0 && + loadedCategories.every((isEmpty) => isEmpty) + ); + }; + + const fetchFavoriteSeries = useCallback( + () => fetchFavoritesByType("Series"), + [fetchFavoritesByType], + ); + const fetchFavoriteMovies = useCallback( + () => fetchFavoritesByType("Movie"), + [fetchFavoritesByType], + ); + const fetchFavoriteEpisodes = useCallback( + () => fetchFavoritesByType("Episode"), + [fetchFavoritesByType], + ); + const fetchFavoriteVideos = useCallback( + () => fetchFavoritesByType("Video"), + [fetchFavoritesByType], + ); + const fetchFavoriteBoxsets = useCallback( + () => fetchFavoritesByType("BoxSet"), + [fetchFavoritesByType], + ); + const fetchFavoritePlaylists = useCallback( + () => fetchFavoritesByType("Playlist"), + [fetchFavoritesByType], + ); + + return ( + + {areAllEmpty() && ( + + + + {t("favorites.noDataTitle")} + + + {t("favorites.noData")} + + + )} + + + + + + + + ); }; diff --git a/translations/de.json b/translations/de.json index ac1b2759..d48e134e 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,471 +1,473 @@ { - "login": { - "username_required": "Benutzername ist erforderlich", - "error_title": "Fehler", - "login_title": "Anmelden", - "login_to_title": "Anmelden bei", - "username_placeholder": "Benutzername", - "password_placeholder": "Passwort", - "login_button": "Anmelden", - "quick_connect": "Schnellverbindung", - "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", - "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", - "got_it": "Verstanden", - "connection_failed": "Verbindung fehlgeschlagen", - "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", - "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", - "change_server": "Server wechseln", - "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", - "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", - "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", - "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", - "there_is_a_server_error": "Es gibt einen Serverfehler", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" - }, - "server": { - "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", - "server_url_placeholder": "http(s)://dein-server.de", - "connect_button": "Verbinden", - "previous_servers": "Vorherige Server", - "clear_button": "Löschen", - "search_for_local_servers": "Nach lokalen Servern suchen", - "searching": "Suche...", - "servers": "Server" - }, - "home": { - "no_internet": "Kein Internet", - "no_items": "Keine Elemente", - "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", - "go_to_downloads": "Gehe zu den Downloads", - "oops": "Ups!", - "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", - "continue_watching": "Weiterschauen", - "next_up": "Als nächstes", - "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", - "suggested_movies": "Empfohlene Filme", - "suggested_episodes": "Empfohlene Episoden", - "intro": { - "welcome_to_streamyfin": "Willkommen bei Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", - "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", - "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", - "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", - "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", - "done_button": "Fertig", - "go_to_settings_button": "Gehe zu den Einstellungen", - "read_more": "Mehr Erfahren" - }, - "settings": { - "settings_title": "Einstellungen", - "log_out_button": "Abmelden", - "user_info": { - "user_info_title": "Benutzerinformationen", - "user": "Benutzer", - "server": "Server", - "token": "Token", - "app_version": "App-Version" - }, - "quick_connect": { - "quick_connect_title": "Schnellverbindung", - "authorize_button": "Schnellverbindung autorisieren", - "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", - "success": "Erfolg", - "quick_connect_autorized": "Schnellverbindung autorisiert", - "error": "Fehler", - "invalid_code": "Ungültiger Code", - "authorize": "Autorisieren" - }, - "media_controls": { - "media_controls_title": "Mediensteuerung", - "forward_skip_length": "Vorspulzeit", - "rewind_length": "Rückspulzeit", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", - "audio_language": "Audio-Sprache", - "audio_hint": "Wähl die Standardsprache für Audio aus.", - "none": "Keine", - "language": "Sprache" - }, - "subtitles": { - "subtitle_title": "Untertitel", - "subtitle_language": "Untertitel-Sprache", - "subtitle_mode": "Untertitel-Modus", - "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", - "subtitle_size": "Untertitel-Größe", - "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", - "none": "Keine", - "language": "Sprache", - "loading": "Lädt", - "modes": { - "Default": "Standard", - "Smart": "Smart", - "Always": "Immer", - "None": "Keine", - "OnlyForced": "Nur erzwungen" - } - }, - "other": { - "other_title": "Sonstiges", - "follow_device_orientation": "Automatische Drehung", - "video_orientation": "Videoausrichtung", - "orientation": "Ausrichtung", - "orientations": { - "DEFAULT": "Standard", - "ALL": "Alle", - "PORTRAIT": "Hochformat", - "PORTRAIT_UP": "Hochformat oben", - "PORTRAIT_DOWN": "Hochformat unten", - "LANDSCAPE": "Querformat", - "LANDSCAPE_LEFT": "Querformat links", - "LANDSCAPE_RIGHT": "Querformat rechts", - "OTHER": "Andere", - "UNKNOWN": "Unbekannt" - }, - "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", - "hide_libraries": "Bibliotheken ausblenden", - "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", - "disable_haptic_feedback": "Haptisches Feedback deaktivieren", - "default_quality": "Standardqualität" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download-Methode", - "remux_max_download": "Maximaler Remux-Download", - "auto_download": "Automatischer Download", - "optimized_versions_server": "Optimierter Versions-Server", - "save_button": "Speichern", - "optimized_server": "Optimierter Server", - "optimized": "Optimiert", - "default": "Standard", - "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", - "server_url": "Server URL", - "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Passwort", - "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", - "save_button": "Speichern", - "clear_button": "Löschen", - "login_button": "Anmelden", - "total_media_requests": "Gesamtanfragen", - "movie_quota_limit": "Film-Anfragelimit", - "movie_quota_days": "Film-Anfragetage", - "tv_quota_limit": "TV-Anfragelimit", - "tv_quota_days": "TV-Anfragetage", - "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", - "unlimited": "Unlimitiert", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Aktiviere Marlin Search", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_marlin": "Erfahre mehr über Marlin.", - "save_button": "Speichern", - "toasts": { - "saved": "Gespeichert" - } - } - }, - "storage": { - "storage_title": "Speicher", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Gerät {{availableSpace}}%", - "size_used": "{{used}} von {{total}} benutzt", - "delete_all_downloaded_files": "Alle Downloads löschen" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Keine Logs verfügbar", - "delete_all_logs": "Alle Logs löschen" - }, - "languages": { - "title": "Sprachen", - "app_language": "App-Sprache", - "app_language_description": "Wähle die Sprache für die App aus.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Fehler beim Löschen von Dateien", - "background_downloads_enabled": "Hintergrunddownloads aktiviert", - "background_downloads_disabled": "Hintergrunddownloads deaktiviert", - "connected": "Verbunden", - "could_not_connect": "Konnte keine Verbindung herstellen", - "invalid_url": "Ungültige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Serien", - "movies": "Filme", - "queue": "Warteschlange", - "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", - "no_items_in_queue": "Keine Elemente in der Warteschlange", - "no_downloaded_items": "Keine heruntergeladenen Elemente", - "delete_all_movies_button": "Alle Filme löschen", - "delete_all_tvseries_button": "Alle TV-Serien löschen", - "delete_all_button": "Alles löschen", - "active_download": "Aktiver Download", - "no_active_downloads": "Keine aktiven Downloads", - "active_downloads": "Aktive Downloads", - "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", - "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", - "back": "Zurück", - "delete": "Löschen", - "something_went_wrong": "Etwas ist schiefgelaufen", - "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", - "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", - "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", - "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", - "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", - "download_cancelled": "Download abgebrochen", - "could_not_cancel_download": "Download konnte nicht abgebrochen werden", - "download_completed": "Download abgeschlossen", - "download_started_for": "Download für {{item}} gestartet", - "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", - "download_stated_for_item": "Download für {{item}} gestartet", - "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", - "download_completed_for_item": "Download für {{item}} ", - "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", - "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", - "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", - "no_response_received_from_server": "Keine Antwort vom Server erhalten", - "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", - "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", - "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", - "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", - "go_to_downloads": "Gehe zu den Downloads" - } - } - }, - "search": { - "search_here": "Hier Suchen...", - "search": "Suche...", - "x_items": "{{count}} Elemente", - "library": "Bibliothek", - "discover": "Entdecken", - "no_results": "Keine Ergebnisse", - "no_results_found_for": "Keine Ergebnisse gefunden für", - "movies": "Filme", - "series": "Serien", - "episodes": "Episoden", - "collections": "Sammlungen", - "actors": "Schauspieler", - "request_movies": "Film anfragen", - "request_series": "Serie anfragen", - "recently_added": "Kürzlich hinzugefügt", - "recent_requests": "Kürzlich angefragt", - "plex_watchlist": "Plex Watchlist", - "trending": "In den Trends", - "popular_movies": "Beliebte Filme", - "movie_genres": "Film-Genres", - "upcoming_movies": "Kommende Filme", - "studios": "Studios", - "popular_tv": "Beliebte TV-Serien", - "tv_genres": "TV-Serien-Genres", - "upcoming_tv": "Kommende TV-Serien", - "networks": "Netzwerke", - "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", - "tmdb_movie_genre": "TMDB Film-Genre", - "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", - "tmdb_tv_genre": "TMDB TV-Serien-Genre", - "tmdb_search": "TMDB Suche", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netzwerk", - "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", - "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" - }, - "library": { - "no_items_found": "Keine Elemente gefunden", - "no_results": "Keine Ergebnisse", - "no_libraries_found": "Keine Bibliotheken gefunden", - "item_types": { - "movies": "Filme", - "series": "Serien", - "boxsets": "Boxsets", - "items": "Elemente" - }, - "options": { - "display": "Display", - "row": "Reihe", - "list": "Liste", - "image_style": "Bildstil", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Titel anzeigen", - "show_stats": "Statistiken anzeigen" - }, - "filters": { - "genres": "Genres", - "years": "Jahre", - "sort_by": "Sortieren nach", - "sort_order": "Sortierreihenfolge", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Serien", - "movies": "Filme", - "episodes": "Episoden", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists" - }, - "custom_links": { - "no_links": "Keine Links" - }, - "player": { - "error": "Fehler", - "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", - "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", - "client_error": "Client-Fehler", - "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", - "message_from_server": "Nachricht vom Server: {{message}}", - "video_has_finished_playing": "Video wurde fertig abgespielt!", - "no_video_source": "Keine Videoquelle...", - "next_episode": "Nächste Episode", - "refresh_tracks": "Spuren aktualisieren", - "subtitle_tracks": "Untertitel-Spuren:", - "audio_tracks": "Audiospuren:", - "playback_state": "Wiedergabestatus:", - "no_data_available": "Keine Daten verfügbar", - "index": "Index:" - }, - "item_card": { - "next_up": "Als Nächstes", - "no_items_to_display": "Keine Elemente zum Anzeigen", - "cast_and_crew": "Besetzung und Crew", - "series": "Serien", - "seasons": "Staffeln", - "season": "Staffel", - "no_episodes_for_this_season": "Keine Episoden für diese Staffel", - "overview": "Überblick", - "more_with": "Mehr mit {{name}}", - "similar_items": "Ähnliche Elemente", - "no_similar_items_found": "Keine ähnlichen Elemente gefunden", - "video": "Video", - "more_details": "Mehr Details", - "quality": "Qualität", - "audio": "Audio", - "subtitles": "Untertitel", - "show_more": "Mehr anzeigen", - "show_less": "Weniger anzeigen", - "appeared_in": "Erschienen in", - "could_not_load_item": "Konnte Element nicht laden", - "none": "Keine", - "download": { - "download_season": "Staffel herunterladen", - "download_series": "Serie herunterladen", - "download_episode": "Episode herunterladen", - "download_movie": "Film herunterladen", - "download_x_item": "{{item_count}} Elemente herunterladen", - "download_button": "Herunterladen", - "using_optimized_server": "Verwende optimierten Server", - "using_default_method": "Verwende Standardmethode" - } - }, - "live_tv": { - "next": "Nächster", - "previous": "Vorheriger", - "live_tv": "Live TV", - "coming_soon": "Demnächst", - "on_now": "Jetzt", - "shows": "Shows", - "movies": "Filme", - "sports": "Sport", - "for_kids": "Für Kinder", - "news": "Nachrichten" - }, - "jellyseerr": { - "confirm": "Bestätigen", - "cancel": "Abbrechen", - "yes": "Ja", - "whats_wrong": "Hast du Probleme?", - "issue_type": "Fehlerart", - "select_an_issue": "Wähle einen Fehlerart aus", - "types": "Arten", - "describe_the_issue": "(optional) Beschreibe das Problem", - "submit_button": "Absenden", - "report_issue_button": "Fehler melden", - "request_button": "Anfragen", - "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", - "failed_to_login": "Fehler beim Anmelden", - "cast": "Besetzung", - "details": "Details", - "status": "Status", - "original_title": "Original Titel", - "series_type": "Serien Typ", - "release_dates": "Veröffentlichungsdaten", - "first_air_date": "Erstausstrahlungsdatum", - "next_air_date": "Nächstes Ausstrahlungsdatum", - "revenue": "Einnahmen", - "budget": "Budget", - "original_language": "Originalsprache", - "production_country": "Produktionsland", - "studios": "Studios", - "network": "Netzwerk", - "currently_streaming_on": "Derzeit im Streaming auf", - "advanced": "Erweitert", - "request_as": "Anfragen als", - "tags": "Tags", - "quality_profile": "Qualitätsprofil", - "root_folder": "Root-Ordner", - "season_all": "Season (all)", - "season_number": "Staffel {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Geboren", - "appearances": "Auftritte", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", - "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", - "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", - "issue_submitted": "Problem eingereicht!", - "requested_item": "{{item}} angefragt!", - "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", - "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" - } - }, - "tabs": { - "home": "Startseite", - "search": "Suche", - "library": "Bibliothek", - "custom_links": "Benutzerdefinierte Links", - "favorites": "Favoriten" - } + "login": { + "username_required": "Benutzername ist erforderlich", + "error_title": "Fehler", + "login_title": "Anmelden", + "login_to_title": "Anmelden bei", + "username_placeholder": "Benutzername", + "password_placeholder": "Passwort", + "login_button": "Anmelden", + "quick_connect": "Schnellverbindung", + "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", + "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", + "got_it": "Verstanden", + "connection_failed": "Verbindung fehlgeschlagen", + "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", + "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", + "change_server": "Server wechseln", + "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", + "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", + "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", + "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", + "there_is_a_server_error": "Es gibt einen Serverfehler", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" + }, + "server": { + "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", + "server_url_placeholder": "http(s)://dein-server.de", + "connect_button": "Verbinden", + "previous_servers": "Vorherige Server", + "clear_button": "Löschen", + "search_for_local_servers": "Nach lokalen Servern suchen", + "searching": "Suche...", + "servers": "Server" + }, + "home": { + "no_internet": "Kein Internet", + "no_items": "Keine Elemente", + "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", + "go_to_downloads": "Gehe zu den Downloads", + "oops": "Ups!", + "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", + "continue_watching": "Weiterschauen", + "next_up": "Als nächstes", + "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", + "suggested_movies": "Empfohlene Filme", + "suggested_episodes": "Empfohlene Episoden", + "intro": { + "welcome_to_streamyfin": "Willkommen bei Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", + "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", + "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", + "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", + "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", + "done_button": "Fertig", + "go_to_settings_button": "Gehe zu den Einstellungen", + "read_more": "Mehr Erfahren" + }, + "settings": { + "settings_title": "Einstellungen", + "log_out_button": "Abmelden", + "user_info": { + "user_info_title": "Benutzerinformationen", + "user": "Benutzer", + "server": "Server", + "token": "Token", + "app_version": "App-Version" + }, + "quick_connect": { + "quick_connect_title": "Schnellverbindung", + "authorize_button": "Schnellverbindung autorisieren", + "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", + "success": "Erfolg", + "quick_connect_autorized": "Schnellverbindung autorisiert", + "error": "Fehler", + "invalid_code": "Ungültiger Code", + "authorize": "Autorisieren" + }, + "media_controls": { + "media_controls_title": "Mediensteuerung", + "forward_skip_length": "Vorspulzeit", + "rewind_length": "Rückspulzeit", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", + "audio_language": "Audio-Sprache", + "audio_hint": "Wähl die Standardsprache für Audio aus.", + "none": "Keine", + "language": "Sprache" + }, + "subtitles": { + "subtitle_title": "Untertitel", + "subtitle_language": "Untertitel-Sprache", + "subtitle_mode": "Untertitel-Modus", + "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", + "subtitle_size": "Untertitel-Größe", + "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", + "none": "Keine", + "language": "Sprache", + "loading": "Lädt", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Immer", + "None": "Keine", + "OnlyForced": "Nur erzwungen" + } + }, + "other": { + "other_title": "Sonstiges", + "follow_device_orientation": "Automatische Drehung", + "video_orientation": "Videoausrichtung", + "orientation": "Ausrichtung", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Hochformat", + "PORTRAIT_UP": "Hochformat oben", + "PORTRAIT_DOWN": "Hochformat unten", + "LANDSCAPE": "Querformat", + "LANDSCAPE_LEFT": "Querformat links", + "LANDSCAPE_RIGHT": "Querformat rechts", + "OTHER": "Andere", + "UNKNOWN": "Unbekannt" + }, + "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", + "hide_libraries": "Bibliotheken ausblenden", + "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", + "disable_haptic_feedback": "Haptisches Feedback deaktivieren", + "default_quality": "Standardqualität" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download-Methode", + "remux_max_download": "Maximaler Remux-Download", + "auto_download": "Automatischer Download", + "optimized_versions_server": "Optimierter Versions-Server", + "save_button": "Speichern", + "optimized_server": "Optimierter Server", + "optimized": "Optimiert", + "default": "Standard", + "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", + "server_url": "Server URL", + "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Passwort", + "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", + "save_button": "Speichern", + "clear_button": "Löschen", + "login_button": "Anmelden", + "total_media_requests": "Gesamtanfragen", + "movie_quota_limit": "Film-Anfragelimit", + "movie_quota_days": "Film-Anfragetage", + "tv_quota_limit": "TV-Anfragelimit", + "tv_quota_days": "TV-Anfragetage", + "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", + "unlimited": "Unlimitiert", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktiviere Marlin Search", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_marlin": "Erfahre mehr über Marlin.", + "save_button": "Speichern", + "toasts": { + "saved": "Gespeichert" + } + } + }, + "storage": { + "storage_title": "Speicher", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Gerät {{availableSpace}}%", + "size_used": "{{used}} von {{total}} benutzt", + "delete_all_downloaded_files": "Alle Downloads löschen" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Keine Logs verfügbar", + "delete_all_logs": "Alle Logs löschen" + }, + "languages": { + "title": "Sprachen", + "app_language": "App-Sprache", + "app_language_description": "Wähle die Sprache für die App aus.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Fehler beim Löschen von Dateien", + "background_downloads_enabled": "Hintergrunddownloads aktiviert", + "background_downloads_disabled": "Hintergrunddownloads deaktiviert", + "connected": "Verbunden", + "could_not_connect": "Konnte keine Verbindung herstellen", + "invalid_url": "Ungültige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Serien", + "movies": "Filme", + "queue": "Warteschlange", + "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", + "no_items_in_queue": "Keine Elemente in der Warteschlange", + "no_downloaded_items": "Keine heruntergeladenen Elemente", + "delete_all_movies_button": "Alle Filme löschen", + "delete_all_tvseries_button": "Alle TV-Serien löschen", + "delete_all_button": "Alles löschen", + "active_download": "Aktiver Download", + "no_active_downloads": "Keine aktiven Downloads", + "active_downloads": "Aktive Downloads", + "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", + "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", + "back": "Zurück", + "delete": "Löschen", + "something_went_wrong": "Etwas ist schiefgelaufen", + "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", + "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", + "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", + "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", + "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", + "download_cancelled": "Download abgebrochen", + "could_not_cancel_download": "Download konnte nicht abgebrochen werden", + "download_completed": "Download abgeschlossen", + "download_started_for": "Download für {{item}} gestartet", + "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", + "download_stated_for_item": "Download für {{item}} gestartet", + "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", + "download_completed_for_item": "Download für {{item}} ", + "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", + "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", + "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", + "no_response_received_from_server": "Keine Antwort vom Server erhalten", + "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", + "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", + "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", + "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", + "go_to_downloads": "Gehe zu den Downloads" + } + } + }, + "search": { + "search_here": "Hier Suchen...", + "search": "Suche...", + "x_items": "{{count}} Elemente", + "library": "Bibliothek", + "discover": "Entdecken", + "no_results": "Keine Ergebnisse", + "no_results_found_for": "Keine Ergebnisse gefunden für", + "movies": "Filme", + "series": "Serien", + "episodes": "Episoden", + "collections": "Sammlungen", + "actors": "Schauspieler", + "request_movies": "Film anfragen", + "request_series": "Serie anfragen", + "recently_added": "Kürzlich hinzugefügt", + "recent_requests": "Kürzlich angefragt", + "plex_watchlist": "Plex Watchlist", + "trending": "In den Trends", + "popular_movies": "Beliebte Filme", + "movie_genres": "Film-Genres", + "upcoming_movies": "Kommende Filme", + "studios": "Studios", + "popular_tv": "Beliebte TV-Serien", + "tv_genres": "TV-Serien-Genres", + "upcoming_tv": "Kommende TV-Serien", + "networks": "Netzwerke", + "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", + "tmdb_movie_genre": "TMDB Film-Genre", + "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", + "tmdb_tv_genre": "TMDB TV-Serien-Genre", + "tmdb_search": "TMDB Suche", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netzwerk", + "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", + "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" + }, + "library": { + "no_items_found": "Keine Elemente gefunden", + "no_results": "Keine Ergebnisse", + "no_libraries_found": "Keine Bibliotheken gefunden", + "item_types": { + "movies": "Filme", + "series": "Serien", + "boxsets": "Boxsets", + "items": "Elemente" + }, + "options": { + "display": "Display", + "row": "Reihe", + "list": "Liste", + "image_style": "Bildstil", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Titel anzeigen", + "show_stats": "Statistiken anzeigen" + }, + "filters": { + "genres": "Genres", + "years": "Jahre", + "sort_by": "Sortieren nach", + "sort_order": "Sortierreihenfolge", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Serien", + "movies": "Filme", + "episodes": "Episoden", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "Noch keine Favoriten", + "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden." + }, + "custom_links": { + "no_links": "Keine Links" + }, + "player": { + "error": "Fehler", + "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", + "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", + "client_error": "Client-Fehler", + "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", + "message_from_server": "Nachricht vom Server: {{message}}", + "video_has_finished_playing": "Video wurde fertig abgespielt!", + "no_video_source": "Keine Videoquelle...", + "next_episode": "Nächste Episode", + "refresh_tracks": "Spuren aktualisieren", + "subtitle_tracks": "Untertitel-Spuren:", + "audio_tracks": "Audiospuren:", + "playback_state": "Wiedergabestatus:", + "no_data_available": "Keine Daten verfügbar", + "index": "Index:" + }, + "item_card": { + "next_up": "Als Nächstes", + "no_items_to_display": "Keine Elemente zum Anzeigen", + "cast_and_crew": "Besetzung und Crew", + "series": "Serien", + "seasons": "Staffeln", + "season": "Staffel", + "no_episodes_for_this_season": "Keine Episoden für diese Staffel", + "overview": "Überblick", + "more_with": "Mehr mit {{name}}", + "similar_items": "Ähnliche Elemente", + "no_similar_items_found": "Keine ähnlichen Elemente gefunden", + "video": "Video", + "more_details": "Mehr Details", + "quality": "Qualität", + "audio": "Audio", + "subtitles": "Untertitel", + "show_more": "Mehr anzeigen", + "show_less": "Weniger anzeigen", + "appeared_in": "Erschienen in", + "could_not_load_item": "Konnte Element nicht laden", + "none": "Keine", + "download": { + "download_season": "Staffel herunterladen", + "download_series": "Serie herunterladen", + "download_episode": "Episode herunterladen", + "download_movie": "Film herunterladen", + "download_x_item": "{{item_count}} Elemente herunterladen", + "download_button": "Herunterladen", + "using_optimized_server": "Verwende optimierten Server", + "using_default_method": "Verwende Standardmethode" + } + }, + "live_tv": { + "next": "Nächster", + "previous": "Vorheriger", + "live_tv": "Live TV", + "coming_soon": "Demnächst", + "on_now": "Jetzt", + "shows": "Shows", + "movies": "Filme", + "sports": "Sport", + "for_kids": "Für Kinder", + "news": "Nachrichten" + }, + "jellyseerr": { + "confirm": "Bestätigen", + "cancel": "Abbrechen", + "yes": "Ja", + "whats_wrong": "Hast du Probleme?", + "issue_type": "Fehlerart", + "select_an_issue": "Wähle einen Fehlerart aus", + "types": "Arten", + "describe_the_issue": "(optional) Beschreibe das Problem", + "submit_button": "Absenden", + "report_issue_button": "Fehler melden", + "request_button": "Anfragen", + "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", + "failed_to_login": "Fehler beim Anmelden", + "cast": "Besetzung", + "details": "Details", + "status": "Status", + "original_title": "Original Titel", + "series_type": "Serien Typ", + "release_dates": "Veröffentlichungsdaten", + "first_air_date": "Erstausstrahlungsdatum", + "next_air_date": "Nächstes Ausstrahlungsdatum", + "revenue": "Einnahmen", + "budget": "Budget", + "original_language": "Originalsprache", + "production_country": "Produktionsland", + "studios": "Studios", + "network": "Netzwerk", + "currently_streaming_on": "Derzeit im Streaming auf", + "advanced": "Erweitert", + "request_as": "Anfragen als", + "tags": "Tags", + "quality_profile": "Qualitätsprofil", + "root_folder": "Root-Ordner", + "season_all": "Season (all)", + "season_number": "Staffel {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Geboren", + "appearances": "Auftritte", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", + "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", + "issue_submitted": "Problem eingereicht!", + "requested_item": "{{item}} angefragt!", + "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", + "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" + } + }, + "tabs": { + "home": "Startseite", + "search": "Suche", + "library": "Bibliothek", + "custom_links": "Benutzerdefinierte Links", + "favorites": "Favoriten" + } } diff --git a/translations/en.json b/translations/en.json index 4c454f9a..d8d18b72 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,475 +1,477 @@ { - "login": { - "username_required": "Username is required", - "error_title": "Error", - "login_title": "Log in", - "login_to_title": "Log in to", - "username_placeholder": "Username", - "password_placeholder": "Password", - "login_button": "Log in", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Enter code {{code}} to login", - "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", - "got_it": "Got it", - "connection_failed": "Connection failed", - "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", - "an_unexpected_error_occured": "An unexpected error occurred", - "change_server": "Change server", - "invalid_username_or_password": "Invalid username or password", - "user_does_not_have_permission_to_log_in": "User does not have permission to log in", - "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", - "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", - "there_is_a_server_error": "There is a server error", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" - }, - "server": { - "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "Connect", - "previous_servers": "previous servers", - "clear_button": "Clear", - "search_for_local_servers": "Search for local servers", - "searching": "Searching...", - "servers": "Servers" - }, - "home": { - "no_internet": "No Internet", - "no_items": "No items", - "no_internet_message": "No worries, you can still watch\ndownloaded content.", - "go_to_downloads": "Go to downloads", - "oops": "Oops!", - "error_message": "Something went wrong.\nPlease log out and in again.", - "continue_watching": "Continue Watching", - "next_up": "Next Up", - "recently_added_in": "Recently Added in {{libraryName}}", - "suggested_movies": "Suggested Movies", - "suggested_episodes": "Suggested Episodes", - "intro": { - "welcome_to_streamyfin": "Welcome to Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", - "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", - "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", - "centralised_settings_plugin_title": "Centralised Settings Plugin", - "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", - "done_button": "Done", - "go_to_settings_button": "Go to settings", - "read_more": "Read more" - }, - "settings": { - "settings_title": "Settings", - "log_out_button": "Log out", - "user_info": { - "user_info_title": "User Info", - "user": "User", - "server": "Server", - "token": "Token", - "app_version": "App Version" - }, - "quick_connect": { - "quick_connect_title": "Quick Connect", - "authorize_button": "Authorize Quick Connect", - "enter_the_quick_connect_code": "Enter the quick connect code...", - "success": "Success", - "quick_connect_autorized": "Quick Connect authorized", - "error": "Error", - "invalid_code": "Invalid code", - "authorize": "Authorize" - }, - "media_controls": { - "media_controls_title": "Media Controls", - "forward_skip_length": "Forward skip length", - "rewind_length": "Rewind length", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Set Audio Track From Previous Item", - "audio_language": "Audio language", - "audio_hint": "Choose a default audio language.", - "none": "None", - "language": "Language" - }, - "subtitles": { - "subtitle_title": "Subtitles", - "subtitle_language": "Subtitle language", - "subtitle_mode": "Subtitle Mode", - "set_subtitle_track": "Set Subtitle Track From Previous Item", - "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", - "none": "None", - "language": "Language", - "loading": "Loading", - "modes": { - "Default": "Default", - "Smart": "Smart", - "Always": "Always", - "None": "None", - "OnlyForced": "OnlyForced" - } - }, - "other": { - "other_title": "Other", - "follow_device_orientation": "Follow device orientation", - "video_orientation": "Video orientation", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Default", - "ALL": "All", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Up", - "PORTRAIT_DOWN": "Portrait Down", - "LANDSCAPE": "Landscape", - "LANDSCAPE_LEFT": "Landscape Left", - "LANDSCAPE_RIGHT": "Landscape Right", - "OTHER": "Other", - "UNKNOWN": "Unknown" - }, - "safe_area_in_controls": "Safe area in controls", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Show Custom Menu Links", - "hide_libraries": "Hide Libraries", - "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", - "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download method", - "remux_max_download": "Remux max download", - "auto_download": "Auto download", - "optimized_versions_server": "Optimized versions server", - "save_button": "Save", - "optimized_server": "Optimized Server", - "optimized": "Optimized", - "default": "Default", - "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", - "read_more_about_optimized_server": "Read more about the optimize server.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", - "server_url": "Server URL", - "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Password", - "password_placeholder": "Enter password for Jellyfin user {{username}}", - "save_button": "Save", - "clear_button": "Clear", - "login_button": "Login", - "total_media_requests": "Total media requests", - "movie_quota_limit": "Movie quota limit", - "movie_quota_days": "Movie quota days", - "tv_quota_limit": "TV quota limit", - "tv_quota_days": "TV quota days", - "reset_jellyseerr_config_button": "Reset Jellyseerr config", - "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Enable Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", - "read_more_about_marlin": "Read more about Marlin.", - "save_button": "Save", - "toasts": { - "saved": "Saved" - } - } - }, - "storage": { - "storage_title": "Storage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Device {{availableSpace}}%", - "size_used": "{{used}} of {{total}} used", - "delete_all_downloaded_files": "Delete All Downloaded Files" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "No logs available", - "delete_all_logs": "Delete all logs" - }, - "languages": { - "title": "Languages", - "app_language": "App language", - "app_language_description": "Select the language for the app.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Error deleting files", - "background_downloads_enabled": "Background downloads enabled", - "background_downloads_disabled": "Background downloads disabled", - "connected": "Connected", - "could_not_connect": "Could not connect", - "invalid_url": "Invalid URL" - } - }, - "sessions": { - "title": "Sessions", - "no_active_sessions": "No active sessions" - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Series", - "movies": "Movies", - "queue": "Queue", - "queue_hint": "Queue and downloads will be lost on app restart", - "no_items_in_queue": "No items in queue", - "no_downloaded_items": "No downloaded items", - "delete_all_movies_button": "Delete all Movies", - "delete_all_tvseries_button": "Delete all TV-Series", - "delete_all_button": "Delete all", - "active_download": "Active download", - "no_active_downloads": "No active downloads", - "active_downloads": "Active downloads", - "new_app_version_requires_re_download": "New app version requires re-download", - "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", - "back": "Back", - "delete": "Delete", - "something_went_wrong": "Something went wrong", - "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methods", - "toasts": { - "you_are_not_allowed_to_download_files": "You are not allowed to download files.", - "deleted_all_movies_successfully": "Deleted all movies successfully!", - "failed_to_delete_all_movies": "Failed to delete all movies", - "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", - "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", - "download_cancelled": "Download cancelled", - "could_not_cancel_download": "Could not cancel download", - "download_completed": "Download completed", - "download_started_for": "Download started for {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", - "download_stated_for_item": "Download started for {{item}}", - "download_failed_for_item": "Download failed for {{item}} - {{error}}", - "download_completed_for_item": "Download completed for {{item}}", - "queued_item_for_optimization": "Queued {{item}} for optimization", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", - "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", - "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", - "go_to_downloads": "Go to downloads" - } - } - }, - "search": { - "search_here": "Search here...", - "search": "Search...", - "x_items": "{{count}} items", - "library": "Library", - "discover": "Discover", - "no_results": "No results", - "no_results_found_for": "No results found for", - "movies": "Movies", - "series": "Series", - "episodes": "Episodes", - "collections": "Collections", - "actors": "Actors", - "request_movies": "Request Movies", - "request_series": "Request Series", - "recently_added": "Recently Added", - "recent_requests": "Recent Requests", - "plex_watchlist": "Plex Watchlist", - "trending": "Trending", - "popular_movies": "Popular Movies", - "movie_genres": "Movie Genres", - "upcoming_movies": "Upcoming Movies", - "studios": "Studios", - "popular_tv": "Popular TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Upcoming TV", - "networks": "Networks", - "tmdb_movie_keyword": "TMDB Movie Keyword", - "tmdb_movie_genre": "TMDB Movie Genre", - "tmdb_tv_keyword": "TMDB TV Keyword", - "tmdb_tv_genre": "TMDB TV Genre", - "tmdb_search": "TMDB Search", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", - "tmdb_tv_streaming_services": "TMDB TV Streaming Services" - }, - "library": { - "no_items_found": "No items found", - "no_results": "No results", - "no_libraries_found": "No libraries found", - "item_types": { - "movies": "movies", - "series": "series", - "boxsets": "box sets", - "items": "items" - }, - "options": { - "display": "Display", - "row": "Row", - "list": "List", - "image_style": "Image style", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Show titles", - "show_stats": "Show stats" - }, - "filters": { - "genres": "Genres", - "years": "Years", - "sort_by": "Sort By", - "sort_order": "Sort Order", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Series", - "movies": "Movies", - "episodes": "Episodes", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists" - }, - "custom_links": { - "no_links": "No links" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Failed to get the stream URL", - "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", - "client_error": "Client error", - "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", - "message_from_server": "Message from server: {{message}}", - "video_has_finished_playing": "Video has finished playing!", - "no_video_source": "No video source...", - "next_episode": "Next Episode", - "refresh_tracks": "Refresh Tracks", - "subtitle_tracks": "Subtitle Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Playback State:", - "no_data_available": "No data available", - "index": "Index:" - }, - "item_card": { - "next_up": "Next up", - "no_items_to_display": "No items to display", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seasons", - "season": "Season", - "no_episodes_for_this_season": "No episodes for this season", - "overview": "Overview", - "more_with": "More with {{name}}", - "similar_items": "Similar items", - "no_similar_items_found": "No similar items found", - "video": "Video", - "more_details": "More details", - "quality": "Quality", - "audio": "Audio", - "subtitles": "Subtitle", - "show_more": "Show more", - "show_less": "Show less", - "appeared_in": "Appeared in", - "could_not_load_item": "Could not load item", - "none": "None", - "download": { - "download_season": "Download Season", - "download_series": "Download Series", - "download_episode": "Download Episode", - "download_movie": "Download Movie", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Using optimized server", - "using_default_method": "Using default method" - } - }, - "live_tv": { - "next": "Next", - "previous": "Previous", - "live_tv": "Live TV", - "coming_soon": "Coming soon", - "on_now": "On now", - "shows": "Shows", - "movies": "Movies", - "sports": "Sports", - "for_kids": "For Kids", - "news": "News" - }, - "jellyseerr": { - "confirm": "Confirm", - "cancel": "Cancel", - "yes": "Yes", - "whats_wrong": "What's wrong?", - "issue_type": "Issue type", - "select_an_issue": "Select an issue", - "types": "Types", - "describe_the_issue": "(optional) Describe the issue...", - "submit_button": "Submit", - "report_issue_button": "Report issue", - "request_button": "Request", - "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", - "failed_to_login": "Failed to login", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Original Title", - "series_type": "Series Type", - "release_dates": "Release Dates", - "first_air_date": "First Air Date", - "next_air_date": "Next Air Date", - "revenue": "Revenue", - "budget": "Budget", - "original_language": "Original Language", - "production_country": "Production Country", - "studios": "Studios", - "network": "Network", - "currently_streaming_on": "Currently Streaming on", - "advanced": "Advanced", - "request_as": "Request As", - "tags": "Tags", - "quality_profile": "Quality Profile", - "root_folder": "Root Folder", - "season_all": "Season (all)", - "season_number": "Season {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Born", - "appearances": "Appearances", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", - "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", - "issue_submitted": "Issue submitted!", - "requested_item": "Requested {{item}}!", - "you_dont_have_permission_to_request": "You don't have permission to request!", - "something_went_wrong_requesting_media": "Something went wrong requesting media!" - } - }, - "tabs": { - "home": "Home", - "search": "Search", - "library": "Library", - "custom_links": "Custom Links", - "favorites": "Favorites" - } + "login": { + "username_required": "Username is required", + "error_title": "Error", + "login_title": "Log in", + "login_to_title": "Log in to", + "username_placeholder": "Username", + "password_placeholder": "Password", + "login_button": "Log in", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Enter code {{code}} to login", + "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", + "got_it": "Got it", + "connection_failed": "Connection failed", + "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", + "an_unexpected_error_occured": "An unexpected error occurred", + "change_server": "Change server", + "invalid_username_or_password": "Invalid username or password", + "user_does_not_have_permission_to_log_in": "User does not have permission to log in", + "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", + "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", + "there_is_a_server_error": "There is a server error", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" + }, + "server": { + "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "Connect", + "previous_servers": "previous servers", + "clear_button": "Clear", + "search_for_local_servers": "Search for local servers", + "searching": "Searching...", + "servers": "Servers" + }, + "home": { + "no_internet": "No Internet", + "no_items": "No items", + "no_internet_message": "No worries, you can still watch\ndownloaded content.", + "go_to_downloads": "Go to downloads", + "oops": "Oops!", + "error_message": "Something went wrong.\nPlease log out and in again.", + "continue_watching": "Continue Watching", + "next_up": "Next Up", + "recently_added_in": "Recently Added in {{libraryName}}", + "suggested_movies": "Suggested Movies", + "suggested_episodes": "Suggested Episodes", + "intro": { + "welcome_to_streamyfin": "Welcome to Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", + "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", + "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", + "centralised_settings_plugin_title": "Centralised Settings Plugin", + "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", + "done_button": "Done", + "go_to_settings_button": "Go to settings", + "read_more": "Read more" + }, + "settings": { + "settings_title": "Settings", + "log_out_button": "Log out", + "user_info": { + "user_info_title": "User Info", + "user": "User", + "server": "Server", + "token": "Token", + "app_version": "App Version" + }, + "quick_connect": { + "quick_connect_title": "Quick Connect", + "authorize_button": "Authorize Quick Connect", + "enter_the_quick_connect_code": "Enter the quick connect code...", + "success": "Success", + "quick_connect_autorized": "Quick Connect authorized", + "error": "Error", + "invalid_code": "Invalid code", + "authorize": "Authorize" + }, + "media_controls": { + "media_controls_title": "Media Controls", + "forward_skip_length": "Forward skip length", + "rewind_length": "Rewind length", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Set Audio Track From Previous Item", + "audio_language": "Audio language", + "audio_hint": "Choose a default audio language.", + "none": "None", + "language": "Language" + }, + "subtitles": { + "subtitle_title": "Subtitles", + "subtitle_language": "Subtitle language", + "subtitle_mode": "Subtitle Mode", + "set_subtitle_track": "Set Subtitle Track From Previous Item", + "subtitle_size": "Subtitle Size", + "subtitle_hint": "Configure subtitle preference.", + "none": "None", + "language": "Language", + "loading": "Loading", + "modes": { + "Default": "Default", + "Smart": "Smart", + "Always": "Always", + "None": "None", + "OnlyForced": "OnlyForced" + } + }, + "other": { + "other_title": "Other", + "follow_device_orientation": "Auto rotate", + "video_orientation": "Video orientation", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Default", + "ALL": "All", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Up", + "PORTRAIT_DOWN": "Portrait Down", + "LANDSCAPE": "Landscape", + "LANDSCAPE_LEFT": "Landscape Left", + "LANDSCAPE_RIGHT": "Landscape Right", + "OTHER": "Other", + "UNKNOWN": "Unknown" + }, + "safe_area_in_controls": "Safe area in controls", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Show Custom Menu Links", + "hide_libraries": "Hide Libraries", + "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", + "disable_haptic_feedback": "Disable Haptic Feedback", + "default_quality": "Default quality" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download method", + "remux_max_download": "Remux max download", + "auto_download": "Auto download", + "optimized_versions_server": "Optimized versions server", + "save_button": "Save", + "optimized_server": "Optimized Server", + "optimized": "Optimized", + "default": "Default", + "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", + "read_more_about_optimized_server": "Read more about the optimize server.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", + "server_url": "Server URL", + "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Password", + "password_placeholder": "Enter password for Jellyfin user {{username}}", + "save_button": "Save", + "clear_button": "Clear", + "login_button": "Login", + "total_media_requests": "Total media requests", + "movie_quota_limit": "Movie quota limit", + "movie_quota_days": "Movie quota days", + "tv_quota_limit": "TV quota limit", + "tv_quota_days": "TV quota days", + "reset_jellyseerr_config_button": "Reset Jellyseerr config", + "unlimited": "Unlimited", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Enable Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", + "read_more_about_marlin": "Read more about Marlin.", + "save_button": "Save", + "toasts": { + "saved": "Saved" + } + } + }, + "storage": { + "storage_title": "Storage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Device {{availableSpace}}%", + "size_used": "{{used}} of {{total}} used", + "delete_all_downloaded_files": "Delete All Downloaded Files" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "No logs available", + "delete_all_logs": "Delete all logs" + }, + "languages": { + "title": "Languages", + "app_language": "App language", + "app_language_description": "Select the language for the app.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Error deleting files", + "background_downloads_enabled": "Background downloads enabled", + "background_downloads_disabled": "Background downloads disabled", + "connected": "Connected", + "could_not_connect": "Could not connect", + "invalid_url": "Invalid URL" + } + }, + "sessions": { + "title": "Sessions", + "no_active_sessions": "No active sessions" + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Series", + "movies": "Movies", + "queue": "Queue", + "queue_hint": "Queue and downloads will be lost on app restart", + "no_items_in_queue": "No items in queue", + "no_downloaded_items": "No downloaded items", + "delete_all_movies_button": "Delete all Movies", + "delete_all_tvseries_button": "Delete all TV-Series", + "delete_all_button": "Delete all", + "active_download": "Active download", + "no_active_downloads": "No active downloads", + "active_downloads": "Active downloads", + "new_app_version_requires_re_download": "New app version requires re-download", + "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", + "back": "Back", + "delete": "Delete", + "something_went_wrong": "Something went wrong", + "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methods", + "toasts": { + "you_are_not_allowed_to_download_files": "You are not allowed to download files.", + "deleted_all_movies_successfully": "Deleted all movies successfully!", + "failed_to_delete_all_movies": "Failed to delete all movies", + "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", + "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", + "download_cancelled": "Download cancelled", + "could_not_cancel_download": "Could not cancel download", + "download_completed": "Download completed", + "download_started_for": "Download started for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", + "download_stated_for_item": "Download started for {{item}}", + "download_failed_for_item": "Download failed for {{item}} - {{error}}", + "download_completed_for_item": "Download completed for {{item}}", + "queued_item_for_optimization": "Queued {{item}} for optimization", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", + "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", + "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", + "go_to_downloads": "Go to downloads" + } + } + }, + "search": { + "search_here": "Search here...", + "search": "Search...", + "x_items": "{{count}} items", + "library": "Library", + "discover": "Discover", + "no_results": "No results", + "no_results_found_for": "No results found for", + "movies": "Movies", + "series": "Series", + "episodes": "Episodes", + "collections": "Collections", + "actors": "Actors", + "request_movies": "Request Movies", + "request_series": "Request Series", + "recently_added": "Recently Added", + "recent_requests": "Recent Requests", + "plex_watchlist": "Plex Watchlist", + "trending": "Trending", + "popular_movies": "Popular Movies", + "movie_genres": "Movie Genres", + "upcoming_movies": "Upcoming Movies", + "studios": "Studios", + "popular_tv": "Popular TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Upcoming TV", + "networks": "Networks", + "tmdb_movie_keyword": "TMDB Movie Keyword", + "tmdb_movie_genre": "TMDB Movie Genre", + "tmdb_tv_keyword": "TMDB TV Keyword", + "tmdb_tv_genre": "TMDB TV Genre", + "tmdb_search": "TMDB Search", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", + "tmdb_tv_streaming_services": "TMDB TV Streaming Services" + }, + "library": { + "no_items_found": "No items found", + "no_results": "No results", + "no_libraries_found": "No libraries found", + "item_types": { + "movies": "movies", + "series": "series", + "boxsets": "box sets", + "items": "items" + }, + "options": { + "display": "Display", + "row": "Row", + "list": "List", + "image_style": "Image style", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Show titles", + "show_stats": "Show stats" + }, + "filters": { + "genres": "Genres", + "years": "Years", + "sort_by": "Sort By", + "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Series", + "movies": "Movies", + "episodes": "Episodes", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "No favorites yet", + "noData": "Mark items as favorites to see them appear here for quick access." + }, + "custom_links": { + "no_links": "No links" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Failed to get the stream URL", + "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", + "client_error": "Client error", + "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", + "message_from_server": "Message from server: {{message}}", + "video_has_finished_playing": "Video has finished playing!", + "no_video_source": "No video source...", + "next_episode": "Next Episode", + "refresh_tracks": "Refresh Tracks", + "subtitle_tracks": "Subtitle Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Playback State:", + "no_data_available": "No data available", + "index": "Index:" + }, + "item_card": { + "next_up": "Next up", + "no_items_to_display": "No items to display", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seasons", + "season": "Season", + "no_episodes_for_this_season": "No episodes for this season", + "overview": "Overview", + "more_with": "More with {{name}}", + "similar_items": "Similar items", + "no_similar_items_found": "No similar items found", + "video": "Video", + "more_details": "More details", + "quality": "Quality", + "audio": "Audio", + "subtitles": "Subtitle", + "show_more": "Show more", + "show_less": "Show less", + "appeared_in": "Appeared in", + "could_not_load_item": "Could not load item", + "none": "None", + "download": { + "download_season": "Download Season", + "download_series": "Download Series", + "download_episode": "Download Episode", + "download_movie": "Download Movie", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Using optimized server", + "using_default_method": "Using default method" + } + }, + "live_tv": { + "next": "Next", + "previous": "Previous", + "live_tv": "Live TV", + "coming_soon": "Coming soon", + "on_now": "On now", + "shows": "Shows", + "movies": "Movies", + "sports": "Sports", + "for_kids": "For Kids", + "news": "News" + }, + "jellyseerr": { + "confirm": "Confirm", + "cancel": "Cancel", + "yes": "Yes", + "whats_wrong": "What's wrong?", + "issue_type": "Issue type", + "select_an_issue": "Select an issue", + "types": "Types", + "describe_the_issue": "(optional) Describe the issue...", + "submit_button": "Submit", + "report_issue_button": "Report issue", + "request_button": "Request", + "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", + "failed_to_login": "Failed to login", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Original Title", + "series_type": "Series Type", + "release_dates": "Release Dates", + "first_air_date": "First Air Date", + "next_air_date": "Next Air Date", + "revenue": "Revenue", + "budget": "Budget", + "original_language": "Original Language", + "production_country": "Production Country", + "studios": "Studios", + "network": "Network", + "currently_streaming_on": "Currently Streaming on", + "advanced": "Advanced", + "request_as": "Request As", + "tags": "Tags", + "quality_profile": "Quality Profile", + "root_folder": "Root Folder", + "season_all": "Season (all)", + "season_number": "Season {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Born", + "appearances": "Appearances", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", + "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", + "issue_submitted": "Issue submitted!", + "requested_item": "Requested {{item}}!", + "you_dont_have_permission_to_request": "You don't have permission to request!", + "something_went_wrong_requesting_media": "Something went wrong requesting media!" + } + }, + "tabs": { + "home": "Home", + "search": "Search", + "library": "Library", + "custom_links": "Custom Links", + "favorites": "Favorites" + } } diff --git a/translations/es.json b/translations/es.json index 95e30124..5182c31f 100644 --- a/translations/es.json +++ b/translations/es.json @@ -1,471 +1,473 @@ { - "login": { - "username_required": "Se requiere un nombre de usuario", - "error_title": "Error", - "login_title": "Iniciar sesión", - "login_to_title": "Iniciar sesión en", - "username_placeholder": "Nombre de usuario", - "password_placeholder": "Contraseña", - "login_button": "Iniciar sesión", - "quick_connect": "Conexión rápida", - "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", - "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", - "got_it": "Entendido", - "connection_failed": "Conexión fallida", - "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", - "an_unexpected_error_occured": "Ha ocurrido un error inesperado", - "change_server": "Cambiar servidor", - "invalid_username_or_password": "Usuario o contraseña inválidos", - "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", - "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", - "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", - "there_is_a_server_error": "Hay un error en el servidor", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" - }, - "server": { - "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", - "server_url_placeholder": "http(s)://tu-servidor.com", - "connect_button": "Conectar", - "previous_servers": "Servidores previos", - "clear_button": "Limpiar", - "search_for_local_servers": "Buscar servidores locales", - "searching": "Buscando...", - "servers": "Servidores" - }, - "home": { - "no_internet": "Sin internet", - "no_items": "No hay ítems", - "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", - "go_to_downloads": "Ir a descargas", - "oops": "¡Vaya!", - "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", - "continue_watching": "Seguir viendo", - "next_up": "A continuación", - "recently_added_in": "Recientemente añadido en {{libraryName}}", - "suggested_movies": "Películas sugeridas", - "suggested_episodes": "Episodios sugeridos", - "intro": { - "welcome_to_streamyfin": "Bienvenido a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", - "features_title": "Características", - "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", - "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", - "downloads_feature_title": "Descargas", - "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", - "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", - "centralised_settings_plugin_title": "Plugin de configuración centralizada", - "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", - "done_button": "Hecho", - "go_to_settings_button": "Ir a la configuración", - "read_more": "Leer más" - }, - "settings": { - "settings_title": "Configuración", - "log_out_button": "Cerrar sesión", - "user_info": { - "user_info_title": "Información de usuario", - "user": "Usuario", - "server": "Servidor", - "token": "Token", - "app_version": "Versión de la app" - }, - "quick_connect": { - "quick_connect_title": "Conexión rápida", - "authorize_button": "Autorizar conexión rápida", - "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", - "success": "Hecho", - "quick_connect_autorized": "Conexión rápida autorizada", - "error": "Error", - "invalid_code": "Código inválido", - "authorize": "Autorizar" - }, - "media_controls": { - "media_controls_title": "Controles de reproducción", - "forward_skip_length": "Longitud de avance", - "rewind_length": "Longitud de retroceso", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Establecer pista del elemento anterior", - "audio_language": "Idioma de audio", - "audio_hint": "Elige un idioma de audio por defecto.", - "none": "Ninguno", - "language": "Idioma" - }, - "subtitles": { - "subtitle_title": "Subtítulos", - "subtitle_language": "Idioma de subtítulos", - "subtitle_mode": "Modo de subtítulos", - "set_subtitle_track": "Establecer pista del elemento anterior", - "subtitle_size": "Tamaño de subtítulos", - "subtitle_hint": "Configurar preferencias de subtítulos.", - "none": "Ninguno", - "language": "Idioma", - "loading": "Cargando", - "modes": { - "Default": "Por defecto", - "Smart": "Inteligente", - "Always": "Siempre", - "None": "Nada", - "OnlyForced": "Solo forzados" - } - }, - "other": { - "other_title": "Otros", - "follow_device_orientation": "Rotación automática", - "video_orientation": "Orientación de vídeo", - "orientation": "Orientación", - "orientations": { - "DEFAULT": "Por defecto", - "ALL": "Todas", - "PORTRAIT": "Vertical", - "PORTRAIT_UP": "Vertical arriba", - "PORTRAIT_DOWN": "Vertical abajo", - "LANDSCAPE": "Horizontal", - "LANDSCAPE_LEFT": "Horizontal izquierda", - "LANDSCAPE_RIGHT": "Horizontal derecha", - "OTHER": "Otra", - "UNKNOWN": "Desconocida" - }, - "safe_area_in_controls": "Área segura en controles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostrar enlaces de menú personalizados", - "hide_libraries": "Ocultar bibliotecas", - "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", - "disable_haptic_feedback": "Desactivar feedback háptico", - "default_quality": "Calidad por defecto" - }, - "downloads": { - "downloads_title": "Descargas", - "download_method": "Método de descarga", - "remux_max_download": "Remux máx. descarga", - "auto_download": "Descarga automática", - "optimized_versions_server": "Servidor de versiones optimizadas", - "save_button": "Guardar", - "optimized_server": "Servidor optimizado", - "optimized": "Optimizado", - "default": "Por defecto", - "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", - "server_url": "URL del servidor", - "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Contrasñea", - "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", - "save_button": "Guardar", - "clear_button": "Limpiar", - "login_button": "Iniciar sesión", - "total_media_requests": "Peticiones totales de medios", - "movie_quota_limit": "Límite de cuota de películas", - "movie_quota_days": "Días de cuota de películas", - "tv_quota_limit": "Límite de cuota de series", - "tv_quota_days": "Días de cuota de series", - "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", - "unlimited": "Ilimitado", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Habilitar búsqueda de Marlin", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto", - "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_marlin": "Leer más sobre Marlin.", - "save_button": "Guardar", - "toasts": { - "saved": "Guardado" - } - } - }, - "storage": { - "storage_title": "Almacenamiento", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} de {{total}} usado", - "delete_all_downloaded_files": "Eliminar todos los archivos descargados" - }, - "intro": { - "show_intro": "Mostrar intro", - "reset_intro": "Restablecer intro" - }, - "logs": { - "logs_title": "Registros", - "no_logs_available": "No hay registros disponibles", - "delete_all_logs": "Eliminar todos los registros" - }, - "languages": { - "title": "Idiomas", - "app_language": "Idioma de la app", - "app_language_description": "Selecciona el idioma de la app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Error al eliminar archivos", - "background_downloads_enabled": "Descargas en segundo plano habilitadas", - "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", - "connected": "Conectado", - "could_not_connect": "No se pudo conectar", - "invalid_url": "URL inválida" - } - }, - "downloads": { - "downloads_title": "Descargas", - "tvseries": "Series", - "movies": "Películas", - "queue": "Cola", - "queue_hint": "La cola de series y películas se perderá al reiniciar la app", - "no_items_in_queue": "No hay ítems en la cola", - "no_downloaded_items": "No hay ítems descargados", - "delete_all_movies_button": "Eliminar todas las películas", - "delete_all_tvseries_button": "Eliminar todas las series", - "delete_all_button": "Eliminar todo", - "active_download": "Descarga activa", - "no_active_downloads": "No hay descargas activas", - "active_downloads": "Descargas activas", - "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", - "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", - "back": "Atrás", - "delete": "Borrar", - "something_went_wrong": "Algo ha salido mal", - "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", - "eta": "{{eta}} restante", - "methods": "Métodos", - "toasts": { - "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", - "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", - "failed_to_delete_all_movies": "Error al eliminar todas las películas", - "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", - "failed_to_delete_all_tvseries": "Error al eliminar todas las series", - "download_cancelled": "Descarga cancelada", - "could_not_cancel_download": "No se pudo cancelar la descarga", - "download_completed": "Descarga completada", - "download_started_for": "Descarga iniciada para {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", - "download_stated_for_item": "Descarga iniciada para {{item}}", - "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", - "download_completed_for_item": "Descarga completada para {{item}}", - "queued_item_for_optimization": "{{item}} en cola para optimización", - "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", - "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", - "no_response_received_from_server": "No se ha recibido respuesta del servidor", - "error_setting_up_the_request": "Error al configurar la petición", - "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", - "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", - "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", - "go_to_downloads": "Ir a descargas" - } - } - }, - "search": { - "search_here": "Buscar aquí...", - "search": "Buscar...", - "x_items": "{{count}} ítems", - "library": "Biblioteca", - "discover": "Descubrir", - "no_results": "Sin resultados", - "no_results_found_for": "No se han encontrado resultados para", - "movies": "Películas", - "series": "Series", - "episodes": "Episodios", - "collections": "Colecciones", - "actors": "Actores", - "request_movies": "Solicitar películas", - "request_series": "Solicitar series", - "recently_added": "Recientemente añadido", - "recent_requests": "Solicitudes recientes", - "plex_watchlist": "Lista de seguimiento de Plex", - "trending": "Trending", - "popular_movies": "Películas populares", - "movie_genres": "Géneros de películas", - "upcoming_movies": "Próximas películas", - "studios": "Estudios", - "popular_tv": "Series populares", - "tv_genres": "Géneros de series", - "upcoming_tv": "Próximas series", - "networks": "Cadenas", - "tmdb_movie_keyword": "Palabra clave de película de TMDB", - "tmdb_movie_genre": "Género de película de TMDB", - "tmdb_tv_keyword": "Palabra clave de serie de TMDB", - "tmdb_tv_genre": "Género de serie de TMDB", - "tmdb_search": "Búsqueda de TMDB", - "tmdb_studio": "Estudio de TMDB", - "tmdb_network": "Cadena de TMDB", - "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", - "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" - }, - "library": { - "no_items_found": "No se han encontrado ítems", - "no_results": "Sin resultados", - "no_libraries_found": "No se han encontrado bibliotecas", - "item_types": { - "movies": "películas", - "series": "series", - "boxsets": "colecciones", - "items": "ítems" - }, - "options": { - "display": "Mostrar", - "row": "Fila", - "list": "Lista", - "image_style": "Estilo de imagen", - "poster": "Poster", - "cover": "Portada", - "show_titles": "Mostrar títulos", - "show_stats": "Mostrar estadísticas" - }, - "filters": { - "genres": "Géneros", - "years": "Años", - "sort_by": "Ordenar por", - "sort_order": "Ordenar", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiquetas" - } - }, - "favorites": { - "series": "Series", - "movies": "Películas", - "episodes": "Episodios", - "videos": "Vídeos", - "boxsets": "Colecciones", - "playlists": "Playlists" - }, - "custom_links": { - "no_links": "Sin enlaces" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Error al obtener la URL del stream", - "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", - "client_error": "Error del cliente", - "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", - "message_from_server": "Mensaje del servidor: {{message}}", - "video_has_finished_playing": "El vídeo ha terminado de reproducirse", - "no_video_source": "No hay fuente de vídeo...", - "next_episode": "Siguiente episodio", - "refresh_tracks": "Refrescar pistas", - "subtitle_tracks": "Pistas de subtítulos:", - "audio_tracks": "Pistas de audio:", - "playback_state": "Estado de la reproducción:", - "no_data_available": "No hay datos disponibles", - "index": "Índice:" - }, - "item_card": { - "next_up": "A continuación", - "no_items_to_display": "No hay ítems para mostrar", - "cast_and_crew": "Reparto y equipo", - "series": "Series", - "seasons": "Temporadas", - "season": "Temporada", - "no_episodes_for_this_season": "No hay episodios para esta temporada", - "overview": "Resumen", - "more_with": "Más con {{name}}", - "similar_items": "Ítems similares", - "no_similar_items_found": "No se han encontrado ítems similares", - "video": "Vídeo", - "more_details": "Más detalles", - "quality": "Calidad", - "audio": "Audio", - "subtitles": "Subtítulos", - "show_more": "Mostrar más", - "show_less": "Mostrar menos", - "appeared_in": "Apareció en", - "could_not_load_item": "No se pudo cargar el ítem", - "none": "Ninguno", - "download": { - "download_season": "Descargar temporada", - "download_series": "Descargar serie", - "download_episode": "Descargar episodio", - "download_movie": "Descargar película", - "download_x_item": "Descargar {{item_count}} ítems", - "download_button": "Descargar", - "using_optimized_server": "Usando servidor optimizado", - "using_default_method": "Usando método por defecto" - } - }, - "live_tv": { - "next": "Siguiente", - "previous": "Anterior", - "live_tv": "TV en directo", - "coming_soon": "Próximamente", - "on_now": "En directo", - "shows": "Programas", - "movies": "Películas", - "sports": "Deportes", - "for_kids": "Para niños", - "news": "Noticias" - }, - "jellyseerr": { - "confirm": "Confirmar", - "cancel": "Cancelar", - "yes": "Sí", - "whats_wrong": "¿Qué pasa?", - "issue_type": "Tipo de problema", - "select_an_issue": "Selecciona un problema", - "types": "Tipos", - "describe_the_issue": "(opcional) Describe el problema...", - "submit_button": "Enviar", - "report_issue_button": "Reportar problema", - "request_button": "Solicitar", - "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", - "failed_to_login": "Error al iniciar sesión", - "cast": "Reparto", - "details": "Detalles", - "status": "Estado", - "original_title": "Título original", - "series_type": "Tipo de serie", - "release_dates": "Fechas de estreno", - "first_air_date": "Primera fecha de emisión", - "next_air_date": "Próxima fecha de emisión", - "revenue": "Ingresos", - "budget": "Presupuesto", - "original_language": "Idioma original", - "production_country": "País de producción", - "studios": "Estudios", - "network": "Cadena", - "currently_streaming_on": "Actualmente en streaming en", - "advanced": "Avanzado", - "request_as": "Solicitar como", - "tags": "Etiquetas", - "quality_profile": "Perfil de calidad", - "root_folder": "Carpeta raíz", - "season_all": "Season (all)", - "season_number": "Temporada {{season_number}}", - "number_episodes": "{{episode_number}} episodios", - "born": "Nacido", - "appearances": "Apariciones", - "toasts": { - "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", - "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", - "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", - "issue_submitted": "¡Problema enviado!", - "requested_item": "¡{{item}} solicitado!", - "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", - "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" - } - }, - "tabs": { - "home": "Inicio", - "search": "Buscar", - "library": "Bibliotecas", - "custom_links": "Enlaces personalizados", - "favorites": "Favoritos" - } + "login": { + "username_required": "Se requiere un nombre de usuario", + "error_title": "Error", + "login_title": "Iniciar sesión", + "login_to_title": "Iniciar sesión en", + "username_placeholder": "Nombre de usuario", + "password_placeholder": "Contraseña", + "login_button": "Iniciar sesión", + "quick_connect": "Conexión rápida", + "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", + "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", + "got_it": "Entendido", + "connection_failed": "Conexión fallida", + "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", + "an_unexpected_error_occured": "Ha ocurrido un error inesperado", + "change_server": "Cambiar servidor", + "invalid_username_or_password": "Usuario o contraseña inválidos", + "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", + "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", + "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", + "there_is_a_server_error": "Hay un error en el servidor", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" + }, + "server": { + "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", + "server_url_placeholder": "http(s)://tu-servidor.com", + "connect_button": "Conectar", + "previous_servers": "Servidores previos", + "clear_button": "Limpiar", + "search_for_local_servers": "Buscar servidores locales", + "searching": "Buscando...", + "servers": "Servidores" + }, + "home": { + "no_internet": "Sin internet", + "no_items": "No hay ítems", + "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", + "go_to_downloads": "Ir a descargas", + "oops": "¡Vaya!", + "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", + "continue_watching": "Seguir viendo", + "next_up": "A continuación", + "recently_added_in": "Recientemente añadido en {{libraryName}}", + "suggested_movies": "Películas sugeridas", + "suggested_episodes": "Episodios sugeridos", + "intro": { + "welcome_to_streamyfin": "Bienvenido a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", + "features_title": "Características", + "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", + "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", + "downloads_feature_title": "Descargas", + "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", + "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", + "centralised_settings_plugin_title": "Plugin de configuración centralizada", + "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", + "done_button": "Hecho", + "go_to_settings_button": "Ir a la configuración", + "read_more": "Leer más" + }, + "settings": { + "settings_title": "Configuración", + "log_out_button": "Cerrar sesión", + "user_info": { + "user_info_title": "Información de usuario", + "user": "Usuario", + "server": "Servidor", + "token": "Token", + "app_version": "Versión de la app" + }, + "quick_connect": { + "quick_connect_title": "Conexión rápida", + "authorize_button": "Autorizar conexión rápida", + "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", + "success": "Hecho", + "quick_connect_autorized": "Conexión rápida autorizada", + "error": "Error", + "invalid_code": "Código inválido", + "authorize": "Autorizar" + }, + "media_controls": { + "media_controls_title": "Controles de reproducción", + "forward_skip_length": "Longitud de avance", + "rewind_length": "Longitud de retroceso", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Establecer pista del elemento anterior", + "audio_language": "Idioma de audio", + "audio_hint": "Elige un idioma de audio por defecto.", + "none": "Ninguno", + "language": "Idioma" + }, + "subtitles": { + "subtitle_title": "Subtítulos", + "subtitle_language": "Idioma de subtítulos", + "subtitle_mode": "Modo de subtítulos", + "set_subtitle_track": "Establecer pista del elemento anterior", + "subtitle_size": "Tamaño de subtítulos", + "subtitle_hint": "Configurar preferencias de subtítulos.", + "none": "Ninguno", + "language": "Idioma", + "loading": "Cargando", + "modes": { + "Default": "Por defecto", + "Smart": "Inteligente", + "Always": "Siempre", + "None": "Nada", + "OnlyForced": "Solo forzados" + } + }, + "other": { + "other_title": "Otros", + "follow_device_orientation": "Rotación automática", + "video_orientation": "Orientación de vídeo", + "orientation": "Orientación", + "orientations": { + "DEFAULT": "Por defecto", + "ALL": "Todas", + "PORTRAIT": "Vertical", + "PORTRAIT_UP": "Vertical arriba", + "PORTRAIT_DOWN": "Vertical abajo", + "LANDSCAPE": "Horizontal", + "LANDSCAPE_LEFT": "Horizontal izquierda", + "LANDSCAPE_RIGHT": "Horizontal derecha", + "OTHER": "Otra", + "UNKNOWN": "Desconocida" + }, + "safe_area_in_controls": "Área segura en controles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostrar enlaces de menú personalizados", + "hide_libraries": "Ocultar bibliotecas", + "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", + "disable_haptic_feedback": "Desactivar feedback háptico", + "default_quality": "Calidad por defecto" + }, + "downloads": { + "downloads_title": "Descargas", + "download_method": "Método de descarga", + "remux_max_download": "Remux máx. descarga", + "auto_download": "Descarga automática", + "optimized_versions_server": "Servidor de versiones optimizadas", + "save_button": "Guardar", + "optimized_server": "Servidor optimizado", + "optimized": "Optimizado", + "default": "Por defecto", + "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", + "server_url": "URL del servidor", + "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Contrasñea", + "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", + "save_button": "Guardar", + "clear_button": "Limpiar", + "login_button": "Iniciar sesión", + "total_media_requests": "Peticiones totales de medios", + "movie_quota_limit": "Límite de cuota de películas", + "movie_quota_days": "Días de cuota de películas", + "tv_quota_limit": "Límite de cuota de series", + "tv_quota_days": "Días de cuota de series", + "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", + "unlimited": "Ilimitado", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Habilitar búsqueda de Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto", + "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_marlin": "Leer más sobre Marlin.", + "save_button": "Guardar", + "toasts": { + "saved": "Guardado" + } + } + }, + "storage": { + "storage_title": "Almacenamiento", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} de {{total}} usado", + "delete_all_downloaded_files": "Eliminar todos los archivos descargados" + }, + "intro": { + "show_intro": "Mostrar intro", + "reset_intro": "Restablecer intro" + }, + "logs": { + "logs_title": "Registros", + "no_logs_available": "No hay registros disponibles", + "delete_all_logs": "Eliminar todos los registros" + }, + "languages": { + "title": "Idiomas", + "app_language": "Idioma de la app", + "app_language_description": "Selecciona el idioma de la app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Error al eliminar archivos", + "background_downloads_enabled": "Descargas en segundo plano habilitadas", + "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", + "connected": "Conectado", + "could_not_connect": "No se pudo conectar", + "invalid_url": "URL inválida" + } + }, + "downloads": { + "downloads_title": "Descargas", + "tvseries": "Series", + "movies": "Películas", + "queue": "Cola", + "queue_hint": "La cola de series y películas se perderá al reiniciar la app", + "no_items_in_queue": "No hay ítems en la cola", + "no_downloaded_items": "No hay ítems descargados", + "delete_all_movies_button": "Eliminar todas las películas", + "delete_all_tvseries_button": "Eliminar todas las series", + "delete_all_button": "Eliminar todo", + "active_download": "Descarga activa", + "no_active_downloads": "No hay descargas activas", + "active_downloads": "Descargas activas", + "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", + "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", + "back": "Atrás", + "delete": "Borrar", + "something_went_wrong": "Algo ha salido mal", + "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", + "eta": "{{eta}} restante", + "methods": "Métodos", + "toasts": { + "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", + "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", + "failed_to_delete_all_movies": "Error al eliminar todas las películas", + "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", + "failed_to_delete_all_tvseries": "Error al eliminar todas las series", + "download_cancelled": "Descarga cancelada", + "could_not_cancel_download": "No se pudo cancelar la descarga", + "download_completed": "Descarga completada", + "download_started_for": "Descarga iniciada para {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", + "download_stated_for_item": "Descarga iniciada para {{item}}", + "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", + "download_completed_for_item": "Descarga completada para {{item}}", + "queued_item_for_optimization": "{{item}} en cola para optimización", + "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", + "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", + "no_response_received_from_server": "No se ha recibido respuesta del servidor", + "error_setting_up_the_request": "Error al configurar la petición", + "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", + "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", + "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", + "go_to_downloads": "Ir a descargas" + } + } + }, + "search": { + "search_here": "Buscar aquí...", + "search": "Buscar...", + "x_items": "{{count}} ítems", + "library": "Biblioteca", + "discover": "Descubrir", + "no_results": "Sin resultados", + "no_results_found_for": "No se han encontrado resultados para", + "movies": "Películas", + "series": "Series", + "episodes": "Episodios", + "collections": "Colecciones", + "actors": "Actores", + "request_movies": "Solicitar películas", + "request_series": "Solicitar series", + "recently_added": "Recientemente añadido", + "recent_requests": "Solicitudes recientes", + "plex_watchlist": "Lista de seguimiento de Plex", + "trending": "Trending", + "popular_movies": "Películas populares", + "movie_genres": "Géneros de películas", + "upcoming_movies": "Próximas películas", + "studios": "Estudios", + "popular_tv": "Series populares", + "tv_genres": "Géneros de series", + "upcoming_tv": "Próximas series", + "networks": "Cadenas", + "tmdb_movie_keyword": "Palabra clave de película de TMDB", + "tmdb_movie_genre": "Género de película de TMDB", + "tmdb_tv_keyword": "Palabra clave de serie de TMDB", + "tmdb_tv_genre": "Género de serie de TMDB", + "tmdb_search": "Búsqueda de TMDB", + "tmdb_studio": "Estudio de TMDB", + "tmdb_network": "Cadena de TMDB", + "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", + "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" + }, + "library": { + "no_items_found": "No se han encontrado ítems", + "no_results": "Sin resultados", + "no_libraries_found": "No se han encontrado bibliotecas", + "item_types": { + "movies": "películas", + "series": "series", + "boxsets": "colecciones", + "items": "ítems" + }, + "options": { + "display": "Mostrar", + "row": "Fila", + "list": "Lista", + "image_style": "Estilo de imagen", + "poster": "Poster", + "cover": "Portada", + "show_titles": "Mostrar títulos", + "show_stats": "Mostrar estadísticas" + }, + "filters": { + "genres": "Géneros", + "years": "Años", + "sort_by": "Ordenar por", + "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiquetas" + } + }, + "favorites": { + "series": "Series", + "movies": "Películas", + "episodes": "Episodios", + "videos": "Vídeos", + "boxsets": "Colecciones", + "playlists": "Playlists", + "noDataTitle": "Aún no hay favoritos", + "noData": "Marca elementos como favoritos para verlos aparecer aquí para un acceso rápido." + }, + "custom_links": { + "no_links": "Sin enlaces" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Error al obtener la URL del stream", + "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", + "client_error": "Error del cliente", + "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", + "message_from_server": "Mensaje del servidor: {{message}}", + "video_has_finished_playing": "El vídeo ha terminado de reproducirse", + "no_video_source": "No hay fuente de vídeo...", + "next_episode": "Siguiente episodio", + "refresh_tracks": "Refrescar pistas", + "subtitle_tracks": "Pistas de subtítulos:", + "audio_tracks": "Pistas de audio:", + "playback_state": "Estado de la reproducción:", + "no_data_available": "No hay datos disponibles", + "index": "Índice:" + }, + "item_card": { + "next_up": "A continuación", + "no_items_to_display": "No hay ítems para mostrar", + "cast_and_crew": "Reparto y equipo", + "series": "Series", + "seasons": "Temporadas", + "season": "Temporada", + "no_episodes_for_this_season": "No hay episodios para esta temporada", + "overview": "Resumen", + "more_with": "Más con {{name}}", + "similar_items": "Ítems similares", + "no_similar_items_found": "No se han encontrado ítems similares", + "video": "Vídeo", + "more_details": "Más detalles", + "quality": "Calidad", + "audio": "Audio", + "subtitles": "Subtítulos", + "show_more": "Mostrar más", + "show_less": "Mostrar menos", + "appeared_in": "Apareció en", + "could_not_load_item": "No se pudo cargar el ítem", + "none": "Ninguno", + "download": { + "download_season": "Descargar temporada", + "download_series": "Descargar serie", + "download_episode": "Descargar episodio", + "download_movie": "Descargar película", + "download_x_item": "Descargar {{item_count}} ítems", + "download_button": "Descargar", + "using_optimized_server": "Usando servidor optimizado", + "using_default_method": "Usando método por defecto" + } + }, + "live_tv": { + "next": "Siguiente", + "previous": "Anterior", + "live_tv": "TV en directo", + "coming_soon": "Próximamente", + "on_now": "En directo", + "shows": "Programas", + "movies": "Películas", + "sports": "Deportes", + "for_kids": "Para niños", + "news": "Noticias" + }, + "jellyseerr": { + "confirm": "Confirmar", + "cancel": "Cancelar", + "yes": "Sí", + "whats_wrong": "¿Qué pasa?", + "issue_type": "Tipo de problema", + "select_an_issue": "Selecciona un problema", + "types": "Tipos", + "describe_the_issue": "(opcional) Describe el problema...", + "submit_button": "Enviar", + "report_issue_button": "Reportar problema", + "request_button": "Solicitar", + "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", + "failed_to_login": "Error al iniciar sesión", + "cast": "Reparto", + "details": "Detalles", + "status": "Estado", + "original_title": "Título original", + "series_type": "Tipo de serie", + "release_dates": "Fechas de estreno", + "first_air_date": "Primera fecha de emisión", + "next_air_date": "Próxima fecha de emisión", + "revenue": "Ingresos", + "budget": "Presupuesto", + "original_language": "Idioma original", + "production_country": "País de producción", + "studios": "Estudios", + "network": "Cadena", + "currently_streaming_on": "Actualmente en streaming en", + "advanced": "Avanzado", + "request_as": "Solicitar como", + "tags": "Etiquetas", + "quality_profile": "Perfil de calidad", + "root_folder": "Carpeta raíz", + "season_all": "Season (all)", + "season_number": "Temporada {{season_number}}", + "number_episodes": "{{episode_number}} episodios", + "born": "Nacido", + "appearances": "Apariciones", + "toasts": { + "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", + "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", + "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", + "issue_submitted": "¡Problema enviado!", + "requested_item": "¡{{item}} solicitado!", + "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", + "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" + } + }, + "tabs": { + "home": "Inicio", + "search": "Buscar", + "library": "Bibliotecas", + "custom_links": "Enlaces personalizados", + "favorites": "Favoritos" + } } diff --git a/translations/fr.json b/translations/fr.json index dbd5b5ef..12f17ef0 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -1,471 +1,473 @@ { - "login": { - "username_required": "Nom d'utilisateur requis", - "error_title": "Erreur", - "login_title": "Se connecter", - "login_to_title": "Se connecter à", - "username_placeholder": "Nom d'utilisateur", - "password_placeholder": "Mot de passe", - "login_button": "Se connecter", - "quick_connect": "Connexion Rapide", - "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", - "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", - "got_it": "D'accord", - "connection_failed": "La connexion a échoué", - "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", - "an_unexpected_error_occured": "Une erreur inattendue s'est produite", - "change_server": "Changer de serveur", - "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", - "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", - "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", - "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", - "there_is_a_server_error": "Il y a une erreur de serveur", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", - "server_url_placeholder": "http(s)://votre-serveur.com", - "connect_button": "Connexion", - "previous_servers": "Serveurs précédents", - "clear_button": "Effacer", - "search_for_local_servers": "Rechercher des serveurs locaux", - "searching": "Recherche...", - "servers": "Serveurs" - }, - "home": { - "no_internet": "Pas d'Internet", - "no_items": "Aucun média", - "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", - "go_to_downloads": "Aller aux téléchargements", - "oops": "Oups!", - "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", - "continue_watching": "Continuer à regarder", - "next_up": "À suivre", - "recently_added_in": "Ajoutés récemment dans {{libraryName}}", - "suggested_movies": "Films suggérés", - "suggested_episodes": "Épisodes suggérés", - "intro": { - "welcome_to_streamyfin": "Bienvenue sur Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", - "features_title": "Fonctionnalités", - "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", - "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", - "downloads_feature_title": "Téléchargements", - "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", - "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", - "centralised_settings_plugin_title": "Plugin de paramètres centralisés", - "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", - "done_button": "Terminé", - "go_to_settings_button": "Allez dans les paramètres", - "read_more": "Lisez-en plus" - }, - "settings": { - "settings_title": "Paramètres", - "log_out_button": "Déconnexion", - "user_info": { - "user_info_title": "Informations utilisateur", - "user": "Utilisateur", - "server": "Serveur", - "token": "Jeton", - "app_version": "Version de l'application" - }, - "quick_connect": { - "quick_connect_title": "Connexion Rapide", - "authorize_button": "Autoriser Connexion Rapide", - "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", - "success": "Succès", - "quick_connect_autorized": "Connexion Rapide autorisé", - "error": "Erreur", - "invalid_code": "Code invalide", - "authorize": "Autoriser" - }, - "media_controls": { - "media_controls_title": "Contrôles Média", - "forward_skip_length": "Durée de saut en avant", - "rewind_length": "Durée de retour en arrière", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Piste audio de l'élément précédent", - "audio_language": "Langue audio", - "audio_hint": "Choisissez une langue audio par défaut.", - "none": "Aucune", - "language": "Langage" - }, - "subtitles": { - "subtitle_title": "Sous-titres", - "subtitle_language": "Langue des sous-titres", - "subtitle_mode": "Mode des sous-titres", - "set_subtitle_track": "Piste de sous-titres de l'élément précédent", - "subtitle_size": "Taille des sous-titres", - "subtitle_hint": "Configurez les préférences des sous-titres.", - "none": "Aucune", - "language": "Langage", - "loading": "Chargement", - "modes": { - "Default": "Par défaut", - "Smart": "Intelligent", - "Always": "Toujours", - "None": "Aucun", - "OnlyForced": "Forcés seulement" - } - }, - "other": { - "other_title": "Autres", - "follow_device_orientation": "Rotation automatique", - "video_orientation": "Orientation vidéo", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Par défaut", - "ALL": "Toutes", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Haut", - "PORTRAIT_DOWN": "Portrait Bas", - "LANDSCAPE": "Paysage", - "LANDSCAPE_LEFT": "Paysage Gauche", - "LANDSCAPE_RIGHT": "Paysage Droite", - "OTHER": "Autre", - "UNKNOWN": "Inconnu" - }, - "safe_area_in_controls": "Zone de sécurité dans les contrôles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Afficher les liens personnalisés", - "hide_libraries": "Cacher des bibliothèques", - "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.", - "disable_haptic_feedback": "Désactiver le retour haptique", - "default_quality": "Qualité par défaut" - }, - "downloads": { - "downloads_title": "Téléchargements", - "download_method": "Méthode de téléchargement", - "remux_max_download": "Téléchargement max remux", - "auto_download": "Téléchargement automatique", - "optimized_versions_server": "Serveur de versions optimisées", - "save_button": "Enregistrer", - "optimized_server": "Serveur optimisé", - "optimized": "Optimisé", - "default": "Par défaut", - "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", - "server_url": "URL du serveur", - "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Mot de passe", - "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", - "save_button": "Enregistrer", - "clear_button": "Effacer", - "login_button": "Connexion", - "total_media_requests": "Total de demandes de médias", - "movie_quota_limit": "Limite de quota de film", - "movie_quota_days": "Jours de quota de film", - "tv_quota_limit": "Limite de quota TV", - "tv_quota_days": "Jours de quota TV", - "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", - "unlimited": "Illimité", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Activer Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port", - "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_marlin": "Lisez-en plus sur Marlin.", - "save_button": "Enregistrer", - "toasts": { - "saved": "Enregistré" - } - } - }, - "storage": { - "storage_title": "Stockage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Appareil {{availableSpace}}%", - "size_used": "{{used}} de {{total}} utilisés", - "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" - }, - "intro": { - "show_intro": "Afficher l'intro", - "reset_intro": "Réinitialiser l'intro" - }, - "logs": { - "logs_title": "Journaux", - "no_logs_available": "Aucun journal disponible", - "delete_all_logs": "Supprimer tous les journaux" - }, - "languages": { - "title": "Langues", - "app_language": "Langue de l'application", - "app_language_description": "Sélectionnez la langue de l'application", - "system": "Système" - }, - "toasts": { - "error_deleting_files": "Erreur lors de la suppression des fichiers", - "background_downloads_enabled": "Téléchargements en arrière-plan activés", - "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", - "connected": "Connecté", - "could_not_connect": "Impossible de se connecter", - "invalid_url": "URL invalide" - } - }, - "downloads": { - "downloads_title": "Téléchargements", - "tvseries": "Séries TV", - "movies": "Films", - "queue": "File d'attente", - "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", - "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", - "no_downloaded_items": "Aucun média téléchargé", - "delete_all_movies_button": "Supprimer tous les films", - "delete_all_tvseries_button": "Supprimer toutes les séries", - "delete_all_button": "Supprimer tout les médias", - "active_download": "Téléchargement actif", - "no_active_downloads": "Aucun téléchargements actifs", - "active_downloads": "Téléchargements actifs", - "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", - "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", - "back": "Retour", - "delete": "Supprimer", - "something_went_wrong": "Quelque chose s'est mal passé", - "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Méthodes", - "toasts": { - "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", - "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", - "failed_to_delete_all_movies": "Échec de la suppression de tous les films", - "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", - "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", - "download_cancelled": "Téléchargement annulé", - "could_not_cancel_download": "Impossible d'annuler le téléchargement", - "download_completed": "Téléchargement terminé", - "download_started_for": "Téléchargement démarré pour {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", - "download_stated_for_item": "Téléchargement démarré pour {{item}}", - "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", - "download_completed_for_item": "Téléchargement terminé pour {{item}}", - "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", - "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", - "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", - "no_response_received_from_server": "Aucune réponse reçue du serveur", - "error_setting_up_the_request": "Erreur lors de la configuration de la demande", - "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", - "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", - "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", - "go_to_downloads": "Aller aux téléchargements" - } - } - }, - "search": { - "search_here": "Rechercher ici...", - "search": "Rechercher...", - "x_items": "{{count}} médias", - "library": "Bibliothèque", - "discover": "Découvrir", - "no_results": "Aucun résultat", - "no_results_found_for": "Aucun résultat trouvé pour", - "movies": "Films", - "series": "Séries", - "episodes": "Épisodes", - "collections": "Collections", - "actors": "Acteurs", - "request_movies": "Demander un film", - "request_series": "Demander une série", - "recently_added": "Ajoutés récemment", - "recent_requests": "Demandes récentes", - "plex_watchlist": "Liste de lecture Plex", - "trending": "Tendance", - "popular_movies": "Films populaires", - "movie_genres": "Genres de films", - "upcoming_movies": "Films à venir", - "studios": "Studios", - "popular_tv": "TV populaire", - "tv_genres": "Genres TV", - "upcoming_tv": "TV à venir", - "networks": "Réseaux", - "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", - "tmdb_movie_genre": "Genre de film TMDB", - "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", - "tmdb_tv_genre": "Genre TV TMDB", - "tmdb_search": "Recherche TMDB", - "tmdb_studio": "Studio TMDB", - "tmdb_network": "Réseau TMDB", - "tmdb_movie_streaming_services": "Services de streaming de films TMDB", - "tmdb_tv_streaming_services": "Services de streaming TV TMDB" - }, - "library": { - "no_items_found": "Aucun média trouvé", - "no_results": "Aucun résultat", - "no_libraries_found": "Aucune bibliothèque trouvée", - "item_types": { - "movies": "films", - "series": "séries", - "boxsets": "coffrets", - "items": "médias" - }, - "options": { - "display": "Affichage", - "row": "Rangée", - "list": "Liste", - "image_style": "Style d'image", - "poster": "Affiche", - "cover": "Couverture", - "show_titles": "Afficher les titres", - "show_stats": "Afficher les statistiques" - }, - "filters": { - "genres": "Genres", - "years": "Années", - "sort_by": "Trier par", - "sort_order": "Ordre de tri", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Séries", - "movies": "Films", - "episodes": "Épisodes", - "videos": "Vidéos", - "boxsets": "Coffrets", - "playlists": "Listes de lecture" - }, - "custom_links": { - "no_links": "Aucuns liens" - }, - "player": { - "error": "Erreur", - "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", - "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", - "client_error": "Erreur client", - "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", - "message_from_server": "Message du serveur: {{message}}", - "video_has_finished_playing": "La vidéo a fini de jouer!", - "no_video_source": "Aucune source vidéo...", - "next_episode": "Épisode suivant", - "refresh_tracks": "Rafraîchir les pistes", - "subtitle_tracks": "Pistes de sous-titres:", - "audio_tracks": "Pistes audio:", - "playback_state": "État de lecture:", - "no_data_available": "Aucune donnée disponible", - "index": "Index:" - }, - "item_card": { - "next_up": "À suivre", - "no_items_to_display": "Aucun médias à afficher", - "cast_and_crew": "Distribution et équipe", - "series": "Séries", - "seasons": "Saisons", - "season": "Saison", - "no_episodes_for_this_season": "Aucun épisode pour cette saison", - "overview": "Aperçu", - "more_with": "Plus avec {{name}}", - "similar_items": "Médias similaires", - "no_similar_items_found": "Aucun média similaire trouvé", - "video": "Vidéo", - "more_details": "Plus de détails", - "quality": "Qualité", - "audio": "Audio", - "subtitles": "Sous-titres", - "show_more": "Afficher plus", - "show_less": "Afficher moins", - "appeared_in": "Apparu dans", - "could_not_load_item": "Impossible de charger le média", - "none": "Aucun", - "download": { - "download_season": "Télécharger la saison", - "download_series": "Télécharger la série", - "download_episode": "Télécharger l'épisode", - "download_movie": "Télécharger le film", - "download_x_item": "Télécharger {{item_count}} médias", - "download_button": "Télécharger", - "using_optimized_server": "Avec le serveur optimisées", - "using_default_method": "Avec la méthode par défaut" - } - }, - "live_tv": { - "next": "Suivant", - "previous": "Précédent", - "live_tv": "TV en direct", - "coming_soon": "Bientôt", - "on_now": "En ce moment", - "shows": "Émissions", - "movies": "Films", - "sports": "Sports", - "for_kids": "Pour enfants", - "news": "Actualités" - }, - "jellyseerr": { - "confirm": "Confirmer", - "cancel": "Annuler", - "yes": "Oui", - "whats_wrong": "Quel est le problème?", - "issue_type": "Type de problème", - "select_an_issue": "Sélectionnez un problème", - "types": "Types", - "describe_the_issue": "(optionnel) Décrivez le problème...", - "submit_button": "Soumettre", - "report_issue_button": "Signaler un problème", - "request_button": "Demander", - "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", - "failed_to_login": "Échec de la connexion", - "cast": "Distribution", - "details": "Détails", - "status": "Statut", - "original_title": "Titre original", - "series_type": "Type de série", - "release_dates": "Dates de sortie", - "first_air_date": "Date de première diffusion", - "next_air_date": "Date de prochaine diffusion", - "revenue": "Revenu", - "budget": "Budget", - "original_language": "Langue originale", - "production_country": "Pays de production", - "studios": "Studios", - "network": "Réseaux", - "currently_streaming_on": "En streaming sur", - "advanced": "Avancé", - "request_as": "Demander en tant que", - "tags": "Tags", - "quality_profile": "Profil de qualité", - "root_folder": "Dossier racine", - "season_all": "Season (all)", - "season_number": "Saison {{season_number}}", - "number_episodes": "{{episode_number}} épisodes", - "born": "Né(e) le", - "appearances": "Apparences", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", - "jellyseerr_test_failed": "Échec du test de Jellyseerr", - "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", - "issue_submitted": "Problème soumis!", - "requested_item": "{{item}}} demandé!", - "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", - "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" - } - }, - "tabs": { - "home": "Accueil", - "search": "Recherche", - "library": "Bibliothèque", - "custom_links": "Liens personnalisés", - "favorites": "Favoris" - } + "login": { + "username_required": "Nom d'utilisateur requis", + "error_title": "Erreur", + "login_title": "Se connecter", + "login_to_title": "Se connecter à", + "username_placeholder": "Nom d'utilisateur", + "password_placeholder": "Mot de passe", + "login_button": "Se connecter", + "quick_connect": "Connexion Rapide", + "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", + "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", + "got_it": "D'accord", + "connection_failed": "La connexion a échoué", + "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", + "an_unexpected_error_occured": "Une erreur inattendue s'est produite", + "change_server": "Changer de serveur", + "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", + "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", + "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", + "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", + "there_is_a_server_error": "Il y a une erreur de serveur", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", + "server_url_placeholder": "http(s)://votre-serveur.com", + "connect_button": "Connexion", + "previous_servers": "Serveurs précédents", + "clear_button": "Effacer", + "search_for_local_servers": "Rechercher des serveurs locaux", + "searching": "Recherche...", + "servers": "Serveurs" + }, + "home": { + "no_internet": "Pas d'Internet", + "no_items": "Aucun média", + "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", + "go_to_downloads": "Aller aux téléchargements", + "oops": "Oups!", + "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", + "continue_watching": "Continuer à regarder", + "next_up": "À suivre", + "recently_added_in": "Ajoutés récemment dans {{libraryName}}", + "suggested_movies": "Films suggérés", + "suggested_episodes": "Épisodes suggérés", + "intro": { + "welcome_to_streamyfin": "Bienvenue sur Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", + "features_title": "Fonctionnalités", + "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", + "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", + "downloads_feature_title": "Téléchargements", + "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", + "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", + "centralised_settings_plugin_title": "Plugin de paramètres centralisés", + "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", + "done_button": "Terminé", + "go_to_settings_button": "Allez dans les paramètres", + "read_more": "Lisez-en plus" + }, + "settings": { + "settings_title": "Paramètres", + "log_out_button": "Déconnexion", + "user_info": { + "user_info_title": "Informations utilisateur", + "user": "Utilisateur", + "server": "Serveur", + "token": "Jeton", + "app_version": "Version de l'application" + }, + "quick_connect": { + "quick_connect_title": "Connexion Rapide", + "authorize_button": "Autoriser Connexion Rapide", + "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", + "success": "Succès", + "quick_connect_autorized": "Connexion Rapide autorisé", + "error": "Erreur", + "invalid_code": "Code invalide", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Contrôles Média", + "forward_skip_length": "Durée de saut en avant", + "rewind_length": "Durée de retour en arrière", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Piste audio de l'élément précédent", + "audio_language": "Langue audio", + "audio_hint": "Choisissez une langue audio par défaut.", + "none": "Aucune", + "language": "Langage" + }, + "subtitles": { + "subtitle_title": "Sous-titres", + "subtitle_language": "Langue des sous-titres", + "subtitle_mode": "Mode des sous-titres", + "set_subtitle_track": "Piste de sous-titres de l'élément précédent", + "subtitle_size": "Taille des sous-titres", + "subtitle_hint": "Configurez les préférences des sous-titres.", + "none": "Aucune", + "language": "Langage", + "loading": "Chargement", + "modes": { + "Default": "Par défaut", + "Smart": "Intelligent", + "Always": "Toujours", + "None": "Aucun", + "OnlyForced": "Forcés seulement" + } + }, + "other": { + "other_title": "Autres", + "follow_device_orientation": "Rotation automatique", + "video_orientation": "Orientation vidéo", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Par défaut", + "ALL": "Toutes", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Haut", + "PORTRAIT_DOWN": "Portrait Bas", + "LANDSCAPE": "Paysage", + "LANDSCAPE_LEFT": "Paysage Gauche", + "LANDSCAPE_RIGHT": "Paysage Droite", + "OTHER": "Autre", + "UNKNOWN": "Inconnu" + }, + "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Afficher les liens personnalisés", + "hide_libraries": "Cacher des bibliothèques", + "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", + "disable_haptic_feedback": "Désactiver le retour haptique", + "default_quality": "Qualité par défaut" + }, + "downloads": { + "downloads_title": "Téléchargements", + "download_method": "Méthode de téléchargement", + "remux_max_download": "Téléchargement max remux", + "auto_download": "Téléchargement automatique", + "optimized_versions_server": "Serveur de versions optimisées", + "save_button": "Enregistrer", + "optimized_server": "Serveur optimisé", + "optimized": "Optimisé", + "default": "Par défaut", + "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", + "server_url": "URL du serveur", + "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Mot de passe", + "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", + "save_button": "Enregistrer", + "clear_button": "Effacer", + "login_button": "Connexion", + "total_media_requests": "Total de demandes de médias", + "movie_quota_limit": "Limite de quota de film", + "movie_quota_days": "Jours de quota de film", + "tv_quota_limit": "Limite de quota TV", + "tv_quota_days": "Jours de quota TV", + "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", + "unlimited": "Illimité", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Activer Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port", + "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_marlin": "Lisez-en plus sur Marlin.", + "save_button": "Enregistrer", + "toasts": { + "saved": "Enregistré" + } + } + }, + "storage": { + "storage_title": "Stockage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Appareil {{availableSpace}}%", + "size_used": "{{used}} de {{total}} utilisés", + "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" + }, + "intro": { + "show_intro": "Afficher l'intro", + "reset_intro": "Réinitialiser l'intro" + }, + "logs": { + "logs_title": "Journaux", + "no_logs_available": "Aucun journal disponible", + "delete_all_logs": "Supprimer tous les journaux" + }, + "languages": { + "title": "Langues", + "app_language": "Langue de l'application", + "app_language_description": "Sélectionnez la langue de l'application", + "system": "Système" + }, + "toasts": { + "error_deleting_files": "Erreur lors de la suppression des fichiers", + "background_downloads_enabled": "Téléchargements en arrière-plan activés", + "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", + "connected": "Connecté", + "could_not_connect": "Impossible de se connecter", + "invalid_url": "URL invalide" + } + }, + "downloads": { + "downloads_title": "Téléchargements", + "tvseries": "Séries TV", + "movies": "Films", + "queue": "File d'attente", + "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", + "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", + "no_downloaded_items": "Aucun média téléchargé", + "delete_all_movies_button": "Supprimer tous les films", + "delete_all_tvseries_button": "Supprimer toutes les séries", + "delete_all_button": "Supprimer tout les médias", + "active_download": "Téléchargement actif", + "no_active_downloads": "Aucun téléchargements actifs", + "active_downloads": "Téléchargements actifs", + "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", + "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", + "back": "Retour", + "delete": "Supprimer", + "something_went_wrong": "Quelque chose s'est mal passé", + "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Méthodes", + "toasts": { + "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", + "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", + "failed_to_delete_all_movies": "Échec de la suppression de tous les films", + "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", + "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", + "download_cancelled": "Téléchargement annulé", + "could_not_cancel_download": "Impossible d'annuler le téléchargement", + "download_completed": "Téléchargement terminé", + "download_started_for": "Téléchargement démarré pour {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", + "download_stated_for_item": "Téléchargement démarré pour {{item}}", + "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", + "download_completed_for_item": "Téléchargement terminé pour {{item}}", + "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", + "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", + "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", + "no_response_received_from_server": "Aucune réponse reçue du serveur", + "error_setting_up_the_request": "Erreur lors de la configuration de la demande", + "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", + "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", + "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", + "go_to_downloads": "Aller aux téléchargements" + } + } + }, + "search": { + "search_here": "Rechercher ici...", + "search": "Rechercher...", + "x_items": "{{count}} médias", + "library": "Bibliothèque", + "discover": "Découvrir", + "no_results": "Aucun résultat", + "no_results_found_for": "Aucun résultat trouvé pour", + "movies": "Films", + "series": "Séries", + "episodes": "Épisodes", + "collections": "Collections", + "actors": "Acteurs", + "request_movies": "Demander un film", + "request_series": "Demander une série", + "recently_added": "Ajoutés récemment", + "recent_requests": "Demandes récentes", + "plex_watchlist": "Liste de lecture Plex", + "trending": "Tendance", + "popular_movies": "Films populaires", + "movie_genres": "Genres de films", + "upcoming_movies": "Films à venir", + "studios": "Studios", + "popular_tv": "TV populaire", + "tv_genres": "Genres TV", + "upcoming_tv": "TV à venir", + "networks": "Réseaux", + "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", + "tmdb_movie_genre": "Genre de film TMDB", + "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", + "tmdb_tv_genre": "Genre TV TMDB", + "tmdb_search": "Recherche TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Réseau TMDB", + "tmdb_movie_streaming_services": "Services de streaming de films TMDB", + "tmdb_tv_streaming_services": "Services de streaming TV TMDB" + }, + "library": { + "no_items_found": "Aucun média trouvé", + "no_results": "Aucun résultat", + "no_libraries_found": "Aucune bibliothèque trouvée", + "item_types": { + "movies": "films", + "series": "séries", + "boxsets": "coffrets", + "items": "médias" + }, + "options": { + "display": "Affichage", + "row": "Rangée", + "list": "Liste", + "image_style": "Style d'image", + "poster": "Affiche", + "cover": "Couverture", + "show_titles": "Afficher les titres", + "show_stats": "Afficher les statistiques" + }, + "filters": { + "genres": "Genres", + "years": "Années", + "sort_by": "Trier par", + "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Séries", + "movies": "Films", + "episodes": "Épisodes", + "videos": "Vidéos", + "boxsets": "Coffrets", + "playlists": "Listes de lecture", + "noDataTitle": "Pas encore de favoris", + "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." + }, + "custom_links": { + "no_links": "Aucuns liens" + }, + "player": { + "error": "Erreur", + "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", + "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", + "client_error": "Erreur client", + "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", + "message_from_server": "Message du serveur: {{message}}", + "video_has_finished_playing": "La vidéo a fini de jouer!", + "no_video_source": "Aucune source vidéo...", + "next_episode": "Épisode suivant", + "refresh_tracks": "Rafraîchir les pistes", + "subtitle_tracks": "Pistes de sous-titres:", + "audio_tracks": "Pistes audio:", + "playback_state": "État de lecture:", + "no_data_available": "Aucune donnée disponible", + "index": "Index:" + }, + "item_card": { + "next_up": "À suivre", + "no_items_to_display": "Aucun médias à afficher", + "cast_and_crew": "Distribution et équipe", + "series": "Séries", + "seasons": "Saisons", + "season": "Saison", + "no_episodes_for_this_season": "Aucun épisode pour cette saison", + "overview": "Aperçu", + "more_with": "Plus avec {{name}}", + "similar_items": "Médias similaires", + "no_similar_items_found": "Aucun média similaire trouvé", + "video": "Vidéo", + "more_details": "Plus de détails", + "quality": "Qualité", + "audio": "Audio", + "subtitles": "Sous-titres", + "show_more": "Afficher plus", + "show_less": "Afficher moins", + "appeared_in": "Apparu dans", + "could_not_load_item": "Impossible de charger le média", + "none": "Aucun", + "download": { + "download_season": "Télécharger la saison", + "download_series": "Télécharger la série", + "download_episode": "Télécharger l'épisode", + "download_movie": "Télécharger le film", + "download_x_item": "Télécharger {{item_count}} médias", + "download_button": "Télécharger", + "using_optimized_server": "Avec le serveur optimisées", + "using_default_method": "Avec la méthode par défaut" + } + }, + "live_tv": { + "next": "Suivant", + "previous": "Précédent", + "live_tv": "TV en direct", + "coming_soon": "Bientôt", + "on_now": "En ce moment", + "shows": "Émissions", + "movies": "Films", + "sports": "Sports", + "for_kids": "Pour enfants", + "news": "Actualités" + }, + "jellyseerr": { + "confirm": "Confirmer", + "cancel": "Annuler", + "yes": "Oui", + "whats_wrong": "Quel est le problème?", + "issue_type": "Type de problème", + "select_an_issue": "Sélectionnez un problème", + "types": "Types", + "describe_the_issue": "(optionnel) Décrivez le problème...", + "submit_button": "Soumettre", + "report_issue_button": "Signaler un problème", + "request_button": "Demander", + "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", + "failed_to_login": "Échec de la connexion", + "cast": "Distribution", + "details": "Détails", + "status": "Statut", + "original_title": "Titre original", + "series_type": "Type de série", + "release_dates": "Dates de sortie", + "first_air_date": "Date de première diffusion", + "next_air_date": "Date de prochaine diffusion", + "revenue": "Revenu", + "budget": "Budget", + "original_language": "Langue originale", + "production_country": "Pays de production", + "studios": "Studios", + "network": "Réseaux", + "currently_streaming_on": "En streaming sur", + "advanced": "Avancé", + "request_as": "Demander en tant que", + "tags": "Tags", + "quality_profile": "Profil de qualité", + "root_folder": "Dossier racine", + "season_all": "Season (all)", + "season_number": "Saison {{season_number}}", + "number_episodes": "{{episode_number}} épisodes", + "born": "Né(e) le", + "appearances": "Apparences", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", + "jellyseerr_test_failed": "Échec du test de Jellyseerr", + "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", + "issue_submitted": "Problème soumis!", + "requested_item": "{{item}}} demandé!", + "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", + "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" + } + }, + "tabs": { + "home": "Accueil", + "search": "Recherche", + "library": "Bibliothèque", + "custom_links": "Liens personnalisés", + "favorites": "Favoris" + } } diff --git a/translations/it.json b/translations/it.json index 98b6a903..38e96cc6 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,471 +1,473 @@ { - "login": { - "username_required": "Nome utente è obbligatorio", - "error_title": "Errore", - "login_title": "Accesso", - "login_to_title": "Accedi a", - "username_placeholder": "Nome utente", - "password_placeholder": "Password", - "login_button": "Accedi", - "quick_connect": "Connessione Rapida", - "enter_code_to_login": "Inserire {{code}} per accedere", - "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", - "got_it": "Capito", - "connection_failed": "Connessione fallita", - "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", - "an_unexpected_error_occured": "Si è verificato un errore inaspettato", - "change_server": "Cambiare il server", - "invalid_username_or_password": "Nome utente o password non validi", - "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", - "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", - "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", - "there_is_a_server_error": "Si è verificato un errore del server", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" - }, - "server": { - "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", - "server_url_placeholder": "http(s)://tuo-server.com", - "connect_button": "Connetti", - "previous_servers": "server precedente", - "clear_button": "Cancella", - "search_for_local_servers": "Ricerca dei server locali", - "searching": "Cercando...", - "servers": "Servers" - }, - "home": { - "no_internet": "Nessun Internet", - "no_items": "Nessun oggetto", - "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", - "go_to_downloads": "Vai agli elementi scaricati", - "oops": "Oops!", - "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", - "continue_watching": "Continua a guardare", - "next_up": "Prossimo", - "recently_added_in": "Aggiunti di recente a {{libraryName}}", - "suggested_movies": "Film consigliati", - "suggested_episodes": "Episodi consigliati", - "intro": { - "welcome_to_streamyfin": "Benvenuto a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", - "features_title": "Funzioni", - "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", - "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", - "downloads_feature_title": "Scaricamento", - "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", - "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", - "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", - "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", - "done_button": "Fatto", - "go_to_settings_button": "Vai alle impostazioni", - "read_more": "Leggi di più" - }, - "settings": { - "settings_title": "Impostazioni", - "log_out_button": "Esci", - "user_info": { - "user_info_title": "Info utente", - "user": "Utente", - "server": "Server", - "token": "Token", - "app_version": "Versione dell'App" - }, - "quick_connect": { - "quick_connect_title": "Connessione Rapida", - "authorize_button": "Autorizza Connessione Rapida", - "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", - "success": "Successo", - "quick_connect_autorized": "Connessione Rapida autorizzata", - "error": "Errore", - "invalid_code": "Codice invalido", - "authorize": "Autorizza" - }, - "media_controls": { - "media_controls_title": "Controlli multimediali", - "forward_skip_length": "Lunghezza del salto in avanti", - "rewind_length": "Lunghezza del riavvolgimento", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Imposta la traccia audio dall'elemento precedente", - "audio_language": "Lingua Audio", - "audio_hint": "Scegli la lingua audio predefinita.", - "none": "Nessuno", - "language": "Lingua" - }, - "subtitles": { - "subtitle_title": "Sottotitoli", - "subtitle_language": "Lingua dei sottotitoli", - "subtitle_mode": "Modalità dei sottotitoli", - "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", - "subtitle_size": "Dimensione dei sottotitoli", - "subtitle_hint": "Configura la preferenza dei sottotitoli.", - "none": "Nessuno", - "language": "Lingua", - "loading": "Caricamento", - "modes": { - "Default": "Predefinito", - "Smart": "Intelligente", - "Always": "Sempre", - "None": "Nessuno", - "OnlyForced": "Solo forzati" - } - }, - "other": { - "other_title": "Altro", - "follow_device_orientation": "Rotazione automatica", - "video_orientation": "Orientamento del video", - "orientation": "Orientamento", - "orientations": { - "DEFAULT": "Predefinito", - "ALL": "Tutto", - "PORTRAIT": "Verticale", - "PORTRAIT_UP": "Verticale sopra", - "PORTRAIT_DOWN": "Verticale sotto", - "LANDSCAPE": "Orizzontale", - "LANDSCAPE_LEFT": "Orizzontale sinitra", - "LANDSCAPE_RIGHT": "Orizzontale destra", - "OTHER": "Altro", - "UNKNOWN": "Sconosciuto" - }, - "safe_area_in_controls": "Area sicura per i controlli", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostra i link del menu personalizzato", - "hide_libraries": "Nascondi Librerie", - "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", - "disable_haptic_feedback": "Disabilita il feedback aptico", - "default_quality": "Qualità predefinita" - }, - "downloads": { - "downloads_title": "Scaricamento", - "download_method": "Metodo per lo scaricamento", - "remux_max_download": "Numero di Remux da scaricare al massimo", - "auto_download": "Scaricamento automatico", - "optimized_versions_server": "Versioni del server di ottimizzazione", - "save_button": "Salva", - "optimized_server": "Server di ottimizzazione", - "optimized": "Ottimizzato", - "default": "Predefinito", - "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta" - }, - "plugins": { - "plugins_title": "Plugin", - "jellyseerr": { - "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", - "server_url": "URL del Server", - "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", - "server_url_placeholder": "URL di Jellyseerr...", - "password": "Password", - "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", - "save_button": "Salva", - "clear_button": "Cancella", - "login_button": "Accedi", - "total_media_requests": "Totale di richieste di media", - "movie_quota_limit": "Limite di quota per i film", - "movie_quota_days": "Giorni di quota per i film", - "tv_quota_limit": "Limite di quota per le serie TV", - "tv_quota_days": "Giorni di quota per le serie TV", - "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", - "unlimited": "Illimitato", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Abilita la ricerca Marlin ", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta", - "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_marlin": "Leggi di più su Marlin.", - "save_button": "Salva", - "toasts": { - "saved": "Salvato" - } - } - }, - "storage": { - "storage_title": "Spazio", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} di {{total}} usato", - "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" - }, - "intro": { - "show_intro": "Mostra intro", - "reset_intro": "Ripristina intro" - }, - "logs": { - "logs_title": "Log", - "no_logs_available": "Nessun log disponibile", - "delete_all_logs": "Cancella tutti i log" - }, - "languages": { - "title": "Lingue", - "app_language": "Lingua dell'App", - "app_language_description": "Selezione la lingua dell'app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Errore nella cancellazione dei file", - "background_downloads_enabled": "Scaricamento in background abilitato", - "background_downloads_disabled": "Scaricamento in background disabilitato", - "connected": "Connesso", - "could_not_connect": "Non è stato possibile connettersi", - "invalid_url": "URL invalido" - } - }, - "downloads": { - "downloads_title": "Scaricati", - "tvseries": "Serie TV", - "movies": "Film", - "queue": "Coda", - "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", - "no_items_in_queue": "Nessun elemento in coda", - "no_downloaded_items": "Nessun elemento scaricato", - "delete_all_movies_button": "Cancella tutti i film", - "delete_all_tvseries_button": "Cancella tutte le serie TV", - "delete_all_button": "Cancella tutti", - "active_download": "Scaricamento in corso", - "no_active_downloads": "Nessun scaricamento in corso", - "active_downloads": "Scaricamenti in corso", - "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", - "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", - "back": "Indietro", - "delete": "Cancella", - "something_went_wrong": "Qualcosa è andato storto", - "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Metodi", - "toasts": { - "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", - "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", - "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", - "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", - "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", - "download_cancelled": "Scaricamento annullato", - "could_not_cancel_download": "Impossibile annullare lo scaricamento", - "download_completed": "Scaricamento completato", - "download_started_for": "Scaricamento iniziato per {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", - "download_stated_for_item": "Scaricamento iniziato per {{item}}", - "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", - "download_completed_for_item": "Scaricamento completato per {{item}}", - "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", - "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", - "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", - "go_to_downloads": "Vai agli elementi scaricati" - } - } - }, - "search": { - "search_here": "Cerca qui...", - "search": "Cerca...", - "x_items": "{{count}} elementi", - "library": "Libreria", - "discover": "Scopri", - "no_results": "Nessun risultato", - "no_results_found_for": "Nessun risultato trovato per", - "movies": "Film", - "series": "Serie", - "episodes": "Episodi", - "collections": "Collezioni", - "actors": "Attori", - "request_movies": "Film Richiesti", - "request_series": "Serie Richieste", - "recently_added": "Aggiunti di Recente", - "recent_requests": "Richiesti di Recente", - "plex_watchlist": "Plex Watchlist", - "trending": "In tendenza", - "popular_movies": "Film Popolari", - "movie_genres": "Generi Film", - "upcoming_movies": "Film in arrivo", - "studios": "Studio", - "popular_tv": "Serie Popolari", - "tv_genres": "Generi Televisivi", - "upcoming_tv": "Serie in Arrivo", - "networks": "Network", - "tmdb_movie_keyword": "TMDB Parola chiave del film", - "tmdb_movie_genre": "TMDB Genere Film", - "tmdb_tv_keyword": "TMDB Parola chiave della serie", - "tmdb_tv_genre": "TMDB Genere Televisivo", - "tmdb_search": "TMDB Cerca", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", - "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" - }, - "library": { - "no_items_found": "Nessun elemento trovato", - "no_results": "Nessun risultato", - "no_libraries_found": "Nessuna libreria trovata", - "item_types": { - "movies": "film", - "series": "serie TV", - "boxsets": "cofanetti", - "items": "elementi" - }, - "options": { - "display": "Display", - "row": "Fila", - "list": "Lista", - "image_style": "Stile dell'immagine", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Mostra titoli", - "show_stats": "Mostra statistiche" - }, - "filters": { - "genres": "Generi", - "years": "Anni", - "sort_by": "Ordina per", - "sort_order": "Criterio di ordinamento", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tag" - } - }, - "favorites": { - "series": "Serie TV", - "movies": "Film", - "episodes": "Episodi", - "videos": "Video", - "boxsets": "Boxset", - "playlists": "Playlist" - }, - "custom_links": { - "no_links": "Nessun link" - }, - "player": { - "error": "Errore", - "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", - "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", - "client_error": "Errore del client", - "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", - "message_from_server": "Messaggio dal server", - "video_has_finished_playing": "La riproduzione del video è terminata!", - "no_video_source": "Nessuna sorgente video...", - "next_episode": "Prossimo Episodio", - "refresh_tracks": "Aggiorna tracce", - "subtitle_tracks": "Tracce di sottotitoli:", - "audio_tracks": "Tracce audio:", - "playback_state": "Stato della riproduzione:", - "no_data_available": "Nessun dato disponibile", - "index": "Indice:" - }, - "item_card": { - "next_up": "Il prossimo", - "no_items_to_display": "Nessun elemento da visualizzare", - "cast_and_crew": "Cast e Equipaggio", - "series": "Serie", - "seasons": "Stagioni", - "season": "Stagione", - "no_episodes_for_this_season": "Nessun episodio per questa stagione", - "overview": "Panoramica", - "more_with": "Altri con {{name}}", - "similar_items": "Elementi simili", - "no_similar_items_found": "Non sono stati trovati elementi simili", - "video": "Video", - "more_details": "Più dettagli", - "quality": "Qualità", - "audio": "Audio", - "subtitles": "Sottotitoli", - "show_more": "Mostra di più", - "show_less": "Mostra di meno", - "appeared_in": "Apparso in", - "could_not_load_item": "Impossibile caricare l'elemento", - "none": "Nessuno", - "download": { - "download_season": "Scarica Stagione", - "download_series": "Scarica Serie", - "download_episode": "Scarica Episodio", - "download_movie": "Scarica Film", - "download_x_item": "Scarica {{item_count}} elementi", - "download_button": "Scarica", - "using_optimized_server": "Utilizzando il server di ottimizzazione", - "using_default_method": "Utilizzando il metodo predefinito" - } - }, - "live_tv": { - "next": "Prossimo", - "previous": "Precedente", - "live_tv": "TV in diretta", - "coming_soon": "Prossimamente", - "on_now": "In onda ora", - "shows": "Programmi", - "movies": "Film", - "sports": "Sport", - "for_kids": "Per Bambini", - "news": "Notiziari" - }, - "jellyseerr": { - "confirm": "Conferma", - "cancel": "Cancella", - "yes": "Si", - "whats_wrong": "Cosa c'è che non va?", - "issue_type": "Tipo di problema", - "select_an_issue": "Seleziona un problema", - "types": "Tipi", - "describe_the_issue": "(facoltativo) Descrivere il problema...", - "submit_button": "Invia", - "report_issue_button": "Segnalare il problema", - "request_button": "Richiedi", - "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", - "failed_to_login": "Accesso non riuscito", - "cast": "Cast", - "details": "Dettagli", - "status": "Stato", - "original_title": "Titolo originale", - "series_type": "Tipo di Serie", - "release_dates": "Date di Uscita", - "first_air_date": "Prima Data di Messa in Onda", - "next_air_date": "Prossima Data di Messa in Onda", - "revenue": "Ricavi", - "budget": "Budget", - "original_language": "Lingua Originale", - "production_country": "Paese di Produzione", - "studios": "Studio", - "network": "Network", - "currently_streaming_on": "Attualmente in streaming su", - "advanced": "Avanzate", - "request_as": "Richiedi Come", - "tags": "Tag", - "quality_profile": "Profilo qualità", - "root_folder": "Cartella radice", - "season_all": "Season (all)", - "season_number": "Stagione {{season_number}}", - "number_episodes": "{{episode_number}} Episodio", - "born": "Nato", - "appearances": "Aspetto", - "toasts": { - "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", - "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", - "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", - "issue_submitted": "Problema inviato!", - "requested_item": "Richiesto {{item}}!", - "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", - "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" - } - }, - "tabs": { - "home": "Home", - "search": "Cerca", - "library": "Libreria", - "custom_links": "Collegamenti personalizzati", - "favorites": "Preferiti" - } + "login": { + "username_required": "Nome utente è obbligatorio", + "error_title": "Errore", + "login_title": "Accesso", + "login_to_title": "Accedi a", + "username_placeholder": "Nome utente", + "password_placeholder": "Password", + "login_button": "Accedi", + "quick_connect": "Connessione Rapida", + "enter_code_to_login": "Inserire {{code}} per accedere", + "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", + "got_it": "Capito", + "connection_failed": "Connessione fallita", + "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", + "an_unexpected_error_occured": "Si è verificato un errore inaspettato", + "change_server": "Cambiare il server", + "invalid_username_or_password": "Nome utente o password non validi", + "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", + "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", + "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", + "there_is_a_server_error": "Si è verificato un errore del server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" + }, + "server": { + "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", + "server_url_placeholder": "http(s)://tuo-server.com", + "connect_button": "Connetti", + "previous_servers": "server precedente", + "clear_button": "Cancella", + "search_for_local_servers": "Ricerca dei server locali", + "searching": "Cercando...", + "servers": "Servers" + }, + "home": { + "no_internet": "Nessun Internet", + "no_items": "Nessun oggetto", + "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", + "go_to_downloads": "Vai agli elementi scaricati", + "oops": "Oops!", + "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", + "continue_watching": "Continua a guardare", + "next_up": "Prossimo", + "recently_added_in": "Aggiunti di recente a {{libraryName}}", + "suggested_movies": "Film consigliati", + "suggested_episodes": "Episodi consigliati", + "intro": { + "welcome_to_streamyfin": "Benvenuto a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", + "features_title": "Funzioni", + "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", + "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", + "downloads_feature_title": "Scaricamento", + "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", + "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", + "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", + "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", + "done_button": "Fatto", + "go_to_settings_button": "Vai alle impostazioni", + "read_more": "Leggi di più" + }, + "settings": { + "settings_title": "Impostazioni", + "log_out_button": "Esci", + "user_info": { + "user_info_title": "Info utente", + "user": "Utente", + "server": "Server", + "token": "Token", + "app_version": "Versione dell'App" + }, + "quick_connect": { + "quick_connect_title": "Connessione Rapida", + "authorize_button": "Autorizza Connessione Rapida", + "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", + "success": "Successo", + "quick_connect_autorized": "Connessione Rapida autorizzata", + "error": "Errore", + "invalid_code": "Codice invalido", + "authorize": "Autorizza" + }, + "media_controls": { + "media_controls_title": "Controlli multimediali", + "forward_skip_length": "Lunghezza del salto in avanti", + "rewind_length": "Lunghezza del riavvolgimento", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Imposta la traccia audio dall'elemento precedente", + "audio_language": "Lingua Audio", + "audio_hint": "Scegli la lingua audio predefinita.", + "none": "Nessuno", + "language": "Lingua" + }, + "subtitles": { + "subtitle_title": "Sottotitoli", + "subtitle_language": "Lingua dei sottotitoli", + "subtitle_mode": "Modalità dei sottotitoli", + "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", + "subtitle_size": "Dimensione dei sottotitoli", + "subtitle_hint": "Configura la preferenza dei sottotitoli.", + "none": "Nessuno", + "language": "Lingua", + "loading": "Caricamento", + "modes": { + "Default": "Predefinito", + "Smart": "Intelligente", + "Always": "Sempre", + "None": "Nessuno", + "OnlyForced": "Solo forzati" + } + }, + "other": { + "other_title": "Altro", + "follow_device_orientation": "Rotazione automatica", + "video_orientation": "Orientamento del video", + "orientation": "Orientamento", + "orientations": { + "DEFAULT": "Predefinito", + "ALL": "Tutto", + "PORTRAIT": "Verticale", + "PORTRAIT_UP": "Verticale sopra", + "PORTRAIT_DOWN": "Verticale sotto", + "LANDSCAPE": "Orizzontale", + "LANDSCAPE_LEFT": "Orizzontale sinitra", + "LANDSCAPE_RIGHT": "Orizzontale destra", + "OTHER": "Altro", + "UNKNOWN": "Sconosciuto" + }, + "safe_area_in_controls": "Area sicura per i controlli", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostra i link del menu personalizzato", + "hide_libraries": "Nascondi Librerie", + "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", + "disable_haptic_feedback": "Disabilita il feedback aptico", + "default_quality": "Qualità predefinita" + }, + "downloads": { + "downloads_title": "Scaricamento", + "download_method": "Metodo per lo scaricamento", + "remux_max_download": "Numero di Remux da scaricare al massimo", + "auto_download": "Scaricamento automatico", + "optimized_versions_server": "Versioni del server di ottimizzazione", + "save_button": "Salva", + "optimized_server": "Server di ottimizzazione", + "optimized": "Ottimizzato", + "default": "Predefinito", + "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta" + }, + "plugins": { + "plugins_title": "Plugin", + "jellyseerr": { + "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", + "server_url": "URL del Server", + "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", + "server_url_placeholder": "URL di Jellyseerr...", + "password": "Password", + "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", + "save_button": "Salva", + "clear_button": "Cancella", + "login_button": "Accedi", + "total_media_requests": "Totale di richieste di media", + "movie_quota_limit": "Limite di quota per i film", + "movie_quota_days": "Giorni di quota per i film", + "tv_quota_limit": "Limite di quota per le serie TV", + "tv_quota_days": "Giorni di quota per le serie TV", + "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", + "unlimited": "Illimitato", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Abilita la ricerca Marlin ", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta", + "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_marlin": "Leggi di più su Marlin.", + "save_button": "Salva", + "toasts": { + "saved": "Salvato" + } + } + }, + "storage": { + "storage_title": "Spazio", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} di {{total}} usato", + "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" + }, + "intro": { + "show_intro": "Mostra intro", + "reset_intro": "Ripristina intro" + }, + "logs": { + "logs_title": "Log", + "no_logs_available": "Nessun log disponibile", + "delete_all_logs": "Cancella tutti i log" + }, + "languages": { + "title": "Lingue", + "app_language": "Lingua dell'App", + "app_language_description": "Selezione la lingua dell'app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Errore nella cancellazione dei file", + "background_downloads_enabled": "Scaricamento in background abilitato", + "background_downloads_disabled": "Scaricamento in background disabilitato", + "connected": "Connesso", + "could_not_connect": "Non è stato possibile connettersi", + "invalid_url": "URL invalido" + } + }, + "downloads": { + "downloads_title": "Scaricati", + "tvseries": "Serie TV", + "movies": "Film", + "queue": "Coda", + "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", + "no_items_in_queue": "Nessun elemento in coda", + "no_downloaded_items": "Nessun elemento scaricato", + "delete_all_movies_button": "Cancella tutti i film", + "delete_all_tvseries_button": "Cancella tutte le serie TV", + "delete_all_button": "Cancella tutti", + "active_download": "Scaricamento in corso", + "no_active_downloads": "Nessun scaricamento in corso", + "active_downloads": "Scaricamenti in corso", + "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", + "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", + "back": "Indietro", + "delete": "Cancella", + "something_went_wrong": "Qualcosa è andato storto", + "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodi", + "toasts": { + "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", + "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", + "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", + "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", + "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", + "download_cancelled": "Scaricamento annullato", + "could_not_cancel_download": "Impossibile annullare lo scaricamento", + "download_completed": "Scaricamento completato", + "download_started_for": "Scaricamento iniziato per {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", + "download_stated_for_item": "Scaricamento iniziato per {{item}}", + "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", + "download_completed_for_item": "Scaricamento completato per {{item}}", + "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", + "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", + "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", + "go_to_downloads": "Vai agli elementi scaricati" + } + } + }, + "search": { + "search_here": "Cerca qui...", + "search": "Cerca...", + "x_items": "{{count}} elementi", + "library": "Libreria", + "discover": "Scopri", + "no_results": "Nessun risultato", + "no_results_found_for": "Nessun risultato trovato per", + "movies": "Film", + "series": "Serie", + "episodes": "Episodi", + "collections": "Collezioni", + "actors": "Attori", + "request_movies": "Film Richiesti", + "request_series": "Serie Richieste", + "recently_added": "Aggiunti di Recente", + "recent_requests": "Richiesti di Recente", + "plex_watchlist": "Plex Watchlist", + "trending": "In tendenza", + "popular_movies": "Film Popolari", + "movie_genres": "Generi Film", + "upcoming_movies": "Film in arrivo", + "studios": "Studio", + "popular_tv": "Serie Popolari", + "tv_genres": "Generi Televisivi", + "upcoming_tv": "Serie in Arrivo", + "networks": "Network", + "tmdb_movie_keyword": "TMDB Parola chiave del film", + "tmdb_movie_genre": "TMDB Genere Film", + "tmdb_tv_keyword": "TMDB Parola chiave della serie", + "tmdb_tv_genre": "TMDB Genere Televisivo", + "tmdb_search": "TMDB Cerca", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", + "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" + }, + "library": { + "no_items_found": "Nessun elemento trovato", + "no_results": "Nessun risultato", + "no_libraries_found": "Nessuna libreria trovata", + "item_types": { + "movies": "film", + "series": "serie TV", + "boxsets": "cofanetti", + "items": "elementi" + }, + "options": { + "display": "Display", + "row": "Fila", + "list": "Lista", + "image_style": "Stile dell'immagine", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Mostra titoli", + "show_stats": "Mostra statistiche" + }, + "filters": { + "genres": "Generi", + "years": "Anni", + "sort_by": "Ordina per", + "sort_order": "Criterio di ordinamento", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tag" + } + }, + "favorites": { + "series": "Serie TV", + "movies": "Film", + "episodes": "Episodi", + "videos": "Video", + "boxsets": "Boxset", + "playlists": "Playlist", + "noDataTitle": "Ancora nessun preferito", + "noData": "Contrassegna gli elementi come preferiti per vederli apparire qui per un accesso rapido." + }, + "custom_links": { + "no_links": "Nessun link" + }, + "player": { + "error": "Errore", + "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", + "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", + "client_error": "Errore del client", + "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", + "message_from_server": "Messaggio dal server", + "video_has_finished_playing": "La riproduzione del video è terminata!", + "no_video_source": "Nessuna sorgente video...", + "next_episode": "Prossimo Episodio", + "refresh_tracks": "Aggiorna tracce", + "subtitle_tracks": "Tracce di sottotitoli:", + "audio_tracks": "Tracce audio:", + "playback_state": "Stato della riproduzione:", + "no_data_available": "Nessun dato disponibile", + "index": "Indice:" + }, + "item_card": { + "next_up": "Il prossimo", + "no_items_to_display": "Nessun elemento da visualizzare", + "cast_and_crew": "Cast e Equipaggio", + "series": "Serie", + "seasons": "Stagioni", + "season": "Stagione", + "no_episodes_for_this_season": "Nessun episodio per questa stagione", + "overview": "Panoramica", + "more_with": "Altri con {{name}}", + "similar_items": "Elementi simili", + "no_similar_items_found": "Non sono stati trovati elementi simili", + "video": "Video", + "more_details": "Più dettagli", + "quality": "Qualità", + "audio": "Audio", + "subtitles": "Sottotitoli", + "show_more": "Mostra di più", + "show_less": "Mostra di meno", + "appeared_in": "Apparso in", + "could_not_load_item": "Impossibile caricare l'elemento", + "none": "Nessuno", + "download": { + "download_season": "Scarica Stagione", + "download_series": "Scarica Serie", + "download_episode": "Scarica Episodio", + "download_movie": "Scarica Film", + "download_x_item": "Scarica {{item_count}} elementi", + "download_button": "Scarica", + "using_optimized_server": "Utilizzando il server di ottimizzazione", + "using_default_method": "Utilizzando il metodo predefinito" + } + }, + "live_tv": { + "next": "Prossimo", + "previous": "Precedente", + "live_tv": "TV in diretta", + "coming_soon": "Prossimamente", + "on_now": "In onda ora", + "shows": "Programmi", + "movies": "Film", + "sports": "Sport", + "for_kids": "Per Bambini", + "news": "Notiziari" + }, + "jellyseerr": { + "confirm": "Conferma", + "cancel": "Cancella", + "yes": "Si", + "whats_wrong": "Cosa c'è che non va?", + "issue_type": "Tipo di problema", + "select_an_issue": "Seleziona un problema", + "types": "Tipi", + "describe_the_issue": "(facoltativo) Descrivere il problema...", + "submit_button": "Invia", + "report_issue_button": "Segnalare il problema", + "request_button": "Richiedi", + "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", + "failed_to_login": "Accesso non riuscito", + "cast": "Cast", + "details": "Dettagli", + "status": "Stato", + "original_title": "Titolo originale", + "series_type": "Tipo di Serie", + "release_dates": "Date di Uscita", + "first_air_date": "Prima Data di Messa in Onda", + "next_air_date": "Prossima Data di Messa in Onda", + "revenue": "Ricavi", + "budget": "Budget", + "original_language": "Lingua Originale", + "production_country": "Paese di Produzione", + "studios": "Studio", + "network": "Network", + "currently_streaming_on": "Attualmente in streaming su", + "advanced": "Avanzate", + "request_as": "Richiedi Come", + "tags": "Tag", + "quality_profile": "Profilo qualità", + "root_folder": "Cartella radice", + "season_all": "Season (all)", + "season_number": "Stagione {{season_number}}", + "number_episodes": "{{episode_number}} Episodio", + "born": "Nato", + "appearances": "Aspetto", + "toasts": { + "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", + "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", + "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", + "issue_submitted": "Problema inviato!", + "requested_item": "Richiesto {{item}}!", + "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", + "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" + } + }, + "tabs": { + "home": "Home", + "search": "Cerca", + "library": "Libreria", + "custom_links": "Collegamenti personalizzati", + "favorites": "Preferiti" + } } diff --git a/translations/ja.json b/translations/ja.json index 7cf14a65..44ac71bd 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -1,470 +1,472 @@ { - "login": { - "username_required": "ユーザー名は必須です", - "error_title": "エラー", - "login_title": "ログイン", - "login_to_title": "ログイン先", - "username_placeholder": "ユーザー名", - "password_placeholder": "パスワード", - "login_button": "ログイン", - "quick_connect": "クイックコネクト", - "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", - "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", - "got_it": "了解", - "connection_failed": "接続に失敗しました", - "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", - "an_unexpected_error_occured": "予期しないエラーが発生しました", - "change_server": "サーバーの変更", - "invalid_username_or_password": "ユーザー名またはパスワードが無効です", - "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", - "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", - "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", - "there_is_a_server_error": "サーバーエラーが発生しました", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" - }, - "server": { - "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "接続", - "previous_servers": "前のサーバー", - "clear_button": "クリア", - "search_for_local_servers": "ローカルサーバーを検索", - "searching": "検索中...", - "servers": "サーバー" - }, - "home": { - "no_internet": "インターネット接続がありません", - "no_items": "アイテムはありません", - "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", - "go_to_downloads": "ダウンロードに移動", - "oops": "おっと!", - "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", - "continue_watching": "続きを見る", - "next_up": "次の動画", - "recently_added_in": "{{libraryName}}に最近追加された", - "suggested_movies": "おすすめ映画", - "suggested_episodes": "おすすめエピソード", - "intro": { - "welcome_to_streamyfin": "Streamyfinへようこそ", - "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", - "features_title": "特長", - "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", - "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", - "downloads_feature_title": "ダウンロード", - "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", - "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", - "centralised_settings_plugin_title": "集中設定プラグイン", - "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", - "done_button": "完了", - "go_to_settings_button": "設定に移動", - "read_more": "続きを読む" - }, - "settings": { - "settings_title": "設定", - "log_out_button": "ログアウト", - "user_info": { - "user_info_title": "ユーザー情報", - "user": "ユーザー", - "server": "サーバー", - "token": "トークン", - "app_version": "アプリバージョン" - }, - "quick_connect": { - "quick_connect_title": "クイックコネクト", - "authorize_button": "クイックコネクトを承認する", - "enter_the_quick_connect_code": "クイックコネクトコードを入力...", - "success": "成功しました", - "quick_connect_autorized": "クイックコネクトが承認されました", - "error": "エラー", - "invalid_code": "無効なコードです", - "authorize": "承認" - }, - "media_controls": { - "media_controls_title": "メディアコントロール", - "forward_skip_length": "スキップの長さ", - "rewind_length": "巻き戻しの長さ", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "オーディオ", - "set_audio_track": "前のアイテムからオーディオトラックを設定", - "audio_language": "オーディオ言語", - "audio_hint": "デフォルトのオーディオ言語を選択します。", - "none": "なし", - "language": "言語" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕の言語", - "subtitle_mode": "字幕モード", - "set_subtitle_track": "前のアイテムから字幕トラックを設定", - "subtitle_size": "字幕サイズ", - "subtitle_hint": "字幕設定を構成します。", - "none": "なし", - "language": "言語", - "loading": "ロード中", - "modes": { - "Default": "デフォルト", - "Smart": "スマート", - "Always": "常に", - "None": "なし", - "OnlyForced": "強制のみ" - } - }, - "other": { - "other_title": "その他", - "follow_device_orientation": "画面の自動回転", - "video_orientation": "動画の向き", - "orientation": "向き", - "orientations": { - "DEFAULT": "デフォルト", - "ALL": "すべて", - "PORTRAIT": "縦", - "PORTRAIT_UP": "縦向き(上)", - "PORTRAIT_DOWN": "縦方向", - "LANDSCAPE": "横方向", - "LANDSCAPE_LEFT": "横方向 左", - "LANDSCAPE_RIGHT": "横方向 右", - "OTHER": "その他", - "UNKNOWN": "不明" - }, - "safe_area_in_controls": "コントロールの安全エリア", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "カスタムメニューのリンクを表示", - "hide_libraries": "ライブラリを非表示", - "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", - "disable_haptic_feedback": "触覚フィードバックを無効にする" - }, - "downloads": { - "downloads_title": "ダウンロード", - "download_method": "ダウンロード方法", - "remux_max_download": "Remux最大ダウンロード数", - "auto_download": "自動ダウンロード", - "optimized_versions_server": "Optimized versionsサーバー", - "save_button": "保存", - "optimized_server": "Optimizedサーバー", - "optimized": "最適化", - "default": "デフォルト", - "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート" - }, - "plugins": { - "plugins_title": "プラグイン", - "jellyseerr": { - "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", - "server_url": "サーバーURL", - "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "パスワード", - "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", - "save_button": "保存", - "clear_button": "クリア", - "login_button": "ログイン", - "total_media_requests": "メディアリクエストの合計", - "movie_quota_limit": "映画のクオータ制限", - "movie_quota_days": "映画のクオータ日数", - "tv_quota_limit": "テレビのクオータ制限", - "tv_quota_days": "テレビのクオータ日数", - "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", - "unlimited": "無制限", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "マーリン検索を有効にする ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート", - "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_marlin": "Marlinについて詳しく読む。", - "save_button": "保存", - "toasts": { - "saved": "保存しました" - } - } - }, - "storage": { - "storage_title": "ストレージ", - "app_usage": "アプリ {{usedSpace}}%", - "phone_usage": "電話 {{availableSpace}}%", - "size_used": "{{used}} / {{total}} 使用済み", - "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" - }, - "intro": { - "show_intro": "イントロを表示", - "reset_intro": "イントロをリセット" - }, - "logs": { - "logs_title": "ログ", - "no_logs_available": "ログがありません", - "delete_all_logs": "すべてのログを削除" - }, - "languages": { - "title": "言語", - "app_language": "アプリの言語", - "app_language_description": "アプリの言語を選択。", - "system": "システム" - }, - "toasts": { - "error_deleting_files": "ファイルの削除エラー", - "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", - "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", - "connected": "接続済み", - "could_not_connect": "接続できません", - "invalid_url": "無効なURL" - } - }, - "downloads": { - "downloads_title": "ダウンロード", - "tvseries": "TVシリーズ", - "movies": "映画", - "queue": "キュー", - "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", - "no_items_in_queue": "キューにアイテムがありません", - "no_downloaded_items": "ダウンロードしたアイテムはありません", - "delete_all_movies_button": "すべての映画を削除", - "delete_all_tvseries_button": "すべてのシリーズを削除", - "delete_all_button": "すべて削除", - "active_download": "アクティブなダウンロード", - "no_active_downloads": "アクティブなダウンロードはありません", - "active_downloads": "アクティブなダウンロード", - "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", - "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", - "back": "戻る", - "delete": "削除", - "something_went_wrong": "問題が発生しました", - "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", - "eta": "ETA {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", - "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", - "failed_to_delete_all_movies": "すべての映画を削除できませんでした", - "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", - "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", - "download_cancelled": "ダウンロードをキャンセルしました", - "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", - "download_completed": "ダウンロードが完了しました", - "download_started_for": "{{item}}のダウンロードが開始されました", - "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", - "download_stated_for_item": "{{item}}のダウンロードが開始されました", - "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", - "download_completed_for_item": "{{item}}のダウンロードが完了しました", - "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", - "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", - "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", - "no_response_received_from_server": "サーバーからの応答がありません", - "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", - "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", - "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", - "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", - "go_to_downloads": "ダウンロードに移動" - } - } - }, - "search": { - "search_here": "ここを検索...", - "search": "検索...", - "x_items": "{{count}}のアイテム", - "library": "ライブラリ", - "discover": "見つける", - "no_results": "結果はありません", - "no_results_found_for": "結果が見つかりませんでした:", - "movies": "映画", - "series": "シリーズ", - "episodes": "エピソード", - "collections": "コレクション", - "actors": "俳優", - "request_movies": "映画をリクエスト", - "request_series": "シリーズをリクエスト", - "recently_added": "最近の追加", - "recent_requests": "最近のリクエスト", - "plex_watchlist": "Plexウォッチリスト", - "trending": "トレンド", - "popular_movies": "人気の映画", - "movie_genres": "映画のジャンル", - "upcoming_movies": "今後リリースされる映画", - "studios": "制作会社", - "popular_tv": "人気のテレビ番組", - "tv_genres": "シリーズのジャンル", - "upcoming_tv": "今後リリースされるシリーズ", - "networks": "ネットワーク", - "tmdb_movie_keyword": "TMDB映画キーワード", - "tmdb_movie_genre": "TMDB映画ジャンル", - "tmdb_tv_keyword": "TMDBシリーズキーワード", - "tmdb_tv_genre": "TMDBシリーズジャンル", - "tmdb_search": "TMDB検索", - "tmdb_studio": "TMDB 制作会社", - "tmdb_network": "TMDB ネットワーク", - "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", - "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" - }, - "library": { - "no_items_found": "アイテムが見つかりません", - "no_results": "検索結果はありません", - "no_libraries_found": "ライブラリが見つかりません", - "item_types": { - "movies": "映画", - "series": "シリーズ", - "boxsets": "ボックスセット", - "items": "アイテム" - }, - "options": { - "display": "表示", - "row": "行", - "list": "リスト", - "image_style": "画像のスタイル", - "poster": "ポスター", - "cover": "カバー", - "show_titles": "タイトルの表示", - "show_stats": "統計を表示" - }, - "filters": { - "genres": "ジャンル", - "years": "年", - "sort_by": "ソート", - "sort_order": "ソート順", - "asc": "Ascending", - "desc": "Descending", - "tags": "タグ" - } - }, - "favorites": { - "series": "シリーズ", - "movies": "映画", - "episodes": "エピソード", - "videos": "ビデオ", - "boxsets": "ボックスセット", - "playlists": "プレイリスト" - }, - "custom_links": { - "no_links": "リンクがありません" - }, - "player": { - "error": "エラー", - "failed_to_get_stream_url": "ストリームURLを取得できませんでした", - "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", - "client_error": "クライアントエラー", - "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", - "message_from_server": "サーバーからのメッセージ", - "video_has_finished_playing": "ビデオの再生が終了しました!", - "no_video_source": "動画ソースがありません...", - "next_episode": "次のエピソード", - "refresh_tracks": "トラックを更新", - "subtitle_tracks": "字幕トラック:", - "audio_tracks": "音声トラック:", - "playback_state": "再生状態:", - "no_data_available": "データなし", - "index": "インデックス:" - }, - "item_card": { - "next_up": "次", - "no_items_to_display": "表示するアイテムがありません", - "cast_and_crew": "キャスト&クルー", - "series": "シリーズ", - "seasons": "シーズン", - "season": "シーズン", - "no_episodes_for_this_season": "このシーズンのエピソードはありません", - "overview": "ストーリー", - "more_with": "{{name}}の詳細", - "similar_items": "類似アイテム", - "no_similar_items_found": "類似のアイテムは見つかりませんでした", - "video": "映像", - "more_details": "さらに詳細を表示", - "quality": "画質", - "audio": "音声", - "subtitles": "字幕", - "show_more": "もっと見る", - "show_less": "少なく表示", - "appeared_in": "出演作品", - "could_not_load_item": "アイテムを読み込めませんでした", - "none": "なし", - "download": { - "download_season": "シーズンをダウンロード", - "download_series": "シリーズをダウンロード", - "download_episode": "エピソードをダウンロード", - "download_movie": "映画をダウンロード", - "download_x_item": "{{item_count}}のアイテムをダウンロード", - "download_button": "ダウンロード", - "using_optimized_server": "Optimizeサーバーを使用する", - "using_default_method": "デフォルトの方法を使用" - } - }, - "live_tv": { - "next": "次", - "previous": "前", - "live_tv": "ライブTV", - "coming_soon": "近日公開", - "on_now": "現在", - "shows": "表示", - "movies": "映画", - "sports": "スポーツ", - "for_kids": "子供向け", - "news": "ニュース" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "キャンセル", - "yes": "はい", - "whats_wrong": "どうしましたか?", - "issue_type": "問題の種類", - "select_an_issue": "問題を選択", - "types": "種類", - "describe_the_issue": "(オプション) 問題を説明してください...", - "submit_button": "送信", - "report_issue_button": "チケットを報告", - "request_button": "リクエスト", - "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", - "failed_to_login": "ログインに失敗しました", - "cast": "出演者", - "details": "詳細", - "status": "状態", - "original_title": "原題", - "series_type": "シリーズタイプ", - "release_dates": "公開日", - "first_air_date": "初放送日", - "next_air_date": "次回放送日", - "revenue": "収益", - "budget": "予算", - "original_language": "オリジナルの言語", - "production_country": "制作国", - "studios": "制作会社", - "network": "ネットワーク", - "currently_streaming_on": "ストリーミング中", - "advanced": "詳細", - "request_as": "別ユーザーとしてリクエスト", - "tags": "タグ", - "quality_profile": "画質プロファイル", - "root_folder": "ルートフォルダ", - "season_all": "Season (all)", - "season_number": "シーズン{{season_number}}", - "number_episodes": "エピソード{{episode_number}}", - "born": "生まれ", - "appearances": "出演", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", - "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", - "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", - "issue_submitted": "チケットを送信しました!", - "requested_item": "{{item}}をリクエスト!", - "you_dont_have_permission_to_request": "リクエストする権限がありません!", - "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" - } - }, - "tabs": { - "home": "ホーム", - "search": "検索", - "library": "ライブラリ", - "custom_links": "カスタムリンク", - "favorites": "お気に入り" - } + "login": { + "username_required": "ユーザー名は必須です", + "error_title": "エラー", + "login_title": "ログイン", + "login_to_title": "ログイン先", + "username_placeholder": "ユーザー名", + "password_placeholder": "パスワード", + "login_button": "ログイン", + "quick_connect": "クイックコネクト", + "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", + "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", + "got_it": "了解", + "connection_failed": "接続に失敗しました", + "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", + "an_unexpected_error_occured": "予期しないエラーが発生しました", + "change_server": "サーバーの変更", + "invalid_username_or_password": "ユーザー名またはパスワードが無効です", + "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", + "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", + "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", + "there_is_a_server_error": "サーバーエラーが発生しました", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" + }, + "server": { + "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "接続", + "previous_servers": "前のサーバー", + "clear_button": "クリア", + "search_for_local_servers": "ローカルサーバーを検索", + "searching": "検索中...", + "servers": "サーバー" + }, + "home": { + "no_internet": "インターネット接続がありません", + "no_items": "アイテムはありません", + "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", + "go_to_downloads": "ダウンロードに移動", + "oops": "おっと!", + "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", + "continue_watching": "続きを見る", + "next_up": "次の動画", + "recently_added_in": "{{libraryName}}に最近追加された", + "suggested_movies": "おすすめ映画", + "suggested_episodes": "おすすめエピソード", + "intro": { + "welcome_to_streamyfin": "Streamyfinへようこそ", + "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", + "features_title": "特長", + "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", + "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", + "downloads_feature_title": "ダウンロード", + "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", + "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", + "centralised_settings_plugin_title": "集中設定プラグイン", + "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", + "done_button": "完了", + "go_to_settings_button": "設定に移動", + "read_more": "続きを読む" + }, + "settings": { + "settings_title": "設定", + "log_out_button": "ログアウト", + "user_info": { + "user_info_title": "ユーザー情報", + "user": "ユーザー", + "server": "サーバー", + "token": "トークン", + "app_version": "アプリバージョン" + }, + "quick_connect": { + "quick_connect_title": "クイックコネクト", + "authorize_button": "クイックコネクトを承認する", + "enter_the_quick_connect_code": "クイックコネクトコードを入力...", + "success": "成功しました", + "quick_connect_autorized": "クイックコネクトが承認されました", + "error": "エラー", + "invalid_code": "無効なコードです", + "authorize": "承認" + }, + "media_controls": { + "media_controls_title": "メディアコントロール", + "forward_skip_length": "スキップの長さ", + "rewind_length": "巻き戻しの長さ", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "オーディオ", + "set_audio_track": "前のアイテムからオーディオトラックを設定", + "audio_language": "オーディオ言語", + "audio_hint": "デフォルトのオーディオ言語を選択します。", + "none": "なし", + "language": "言語" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕の言語", + "subtitle_mode": "字幕モード", + "set_subtitle_track": "前のアイテムから字幕トラックを設定", + "subtitle_size": "字幕サイズ", + "subtitle_hint": "字幕設定を構成します。", + "none": "なし", + "language": "言語", + "loading": "ロード中", + "modes": { + "Default": "デフォルト", + "Smart": "スマート", + "Always": "常に", + "None": "なし", + "OnlyForced": "強制のみ" + } + }, + "other": { + "other_title": "その他", + "follow_device_orientation": "画面の自動回転", + "video_orientation": "動画の向き", + "orientation": "向き", + "orientations": { + "DEFAULT": "デフォルト", + "ALL": "すべて", + "PORTRAIT": "縦", + "PORTRAIT_UP": "縦向き(上)", + "PORTRAIT_DOWN": "縦方向", + "LANDSCAPE": "横方向", + "LANDSCAPE_LEFT": "横方向 左", + "LANDSCAPE_RIGHT": "横方向 右", + "OTHER": "その他", + "UNKNOWN": "不明" + }, + "safe_area_in_controls": "コントロールの安全エリア", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "カスタムメニューのリンクを表示", + "hide_libraries": "ライブラリを非表示", + "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", + "disable_haptic_feedback": "触覚フィードバックを無効にする" + }, + "downloads": { + "downloads_title": "ダウンロード", + "download_method": "ダウンロード方法", + "remux_max_download": "Remux最大ダウンロード数", + "auto_download": "自動ダウンロード", + "optimized_versions_server": "Optimized versionsサーバー", + "save_button": "保存", + "optimized_server": "Optimizedサーバー", + "optimized": "最適化", + "default": "デフォルト", + "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート" + }, + "plugins": { + "plugins_title": "プラグイン", + "jellyseerr": { + "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", + "server_url": "サーバーURL", + "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "パスワード", + "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", + "save_button": "保存", + "clear_button": "クリア", + "login_button": "ログイン", + "total_media_requests": "メディアリクエストの合計", + "movie_quota_limit": "映画のクオータ制限", + "movie_quota_days": "映画のクオータ日数", + "tv_quota_limit": "テレビのクオータ制限", + "tv_quota_days": "テレビのクオータ日数", + "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", + "unlimited": "無制限", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "マーリン検索を有効にする ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート", + "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_marlin": "Marlinについて詳しく読む。", + "save_button": "保存", + "toasts": { + "saved": "保存しました" + } + } + }, + "storage": { + "storage_title": "ストレージ", + "app_usage": "アプリ {{usedSpace}}%", + "phone_usage": "電話 {{availableSpace}}%", + "size_used": "{{used}} / {{total}} 使用済み", + "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" + }, + "intro": { + "show_intro": "イントロを表示", + "reset_intro": "イントロをリセット" + }, + "logs": { + "logs_title": "ログ", + "no_logs_available": "ログがありません", + "delete_all_logs": "すべてのログを削除" + }, + "languages": { + "title": "言語", + "app_language": "アプリの言語", + "app_language_description": "アプリの言語を選択。", + "system": "システム" + }, + "toasts": { + "error_deleting_files": "ファイルの削除エラー", + "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", + "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", + "connected": "接続済み", + "could_not_connect": "接続できません", + "invalid_url": "無効なURL" + } + }, + "downloads": { + "downloads_title": "ダウンロード", + "tvseries": "TVシリーズ", + "movies": "映画", + "queue": "キュー", + "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", + "no_items_in_queue": "キューにアイテムがありません", + "no_downloaded_items": "ダウンロードしたアイテムはありません", + "delete_all_movies_button": "すべての映画を削除", + "delete_all_tvseries_button": "すべてのシリーズを削除", + "delete_all_button": "すべて削除", + "active_download": "アクティブなダウンロード", + "no_active_downloads": "アクティブなダウンロードはありません", + "active_downloads": "アクティブなダウンロード", + "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", + "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", + "back": "戻る", + "delete": "削除", + "something_went_wrong": "問題が発生しました", + "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", + "eta": "ETA {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", + "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", + "failed_to_delete_all_movies": "すべての映画を削除できませんでした", + "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", + "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", + "download_cancelled": "ダウンロードをキャンセルしました", + "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", + "download_completed": "ダウンロードが完了しました", + "download_started_for": "{{item}}のダウンロードが開始されました", + "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", + "download_stated_for_item": "{{item}}のダウンロードが開始されました", + "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", + "download_completed_for_item": "{{item}}のダウンロードが完了しました", + "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", + "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", + "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", + "no_response_received_from_server": "サーバーからの応答がありません", + "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", + "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", + "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", + "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", + "go_to_downloads": "ダウンロードに移動" + } + } + }, + "search": { + "search_here": "ここを検索...", + "search": "検索...", + "x_items": "{{count}}のアイテム", + "library": "ライブラリ", + "discover": "見つける", + "no_results": "結果はありません", + "no_results_found_for": "結果が見つかりませんでした:", + "movies": "映画", + "series": "シリーズ", + "episodes": "エピソード", + "collections": "コレクション", + "actors": "俳優", + "request_movies": "映画をリクエスト", + "request_series": "シリーズをリクエスト", + "recently_added": "最近の追加", + "recent_requests": "最近のリクエスト", + "plex_watchlist": "Plexウォッチリスト", + "trending": "トレンド", + "popular_movies": "人気の映画", + "movie_genres": "映画のジャンル", + "upcoming_movies": "今後リリースされる映画", + "studios": "制作会社", + "popular_tv": "人気のテレビ番組", + "tv_genres": "シリーズのジャンル", + "upcoming_tv": "今後リリースされるシリーズ", + "networks": "ネットワーク", + "tmdb_movie_keyword": "TMDB映画キーワード", + "tmdb_movie_genre": "TMDB映画ジャンル", + "tmdb_tv_keyword": "TMDBシリーズキーワード", + "tmdb_tv_genre": "TMDBシリーズジャンル", + "tmdb_search": "TMDB検索", + "tmdb_studio": "TMDB 制作会社", + "tmdb_network": "TMDB ネットワーク", + "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", + "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" + }, + "library": { + "no_items_found": "アイテムが見つかりません", + "no_results": "検索結果はありません", + "no_libraries_found": "ライブラリが見つかりません", + "item_types": { + "movies": "映画", + "series": "シリーズ", + "boxsets": "ボックスセット", + "items": "アイテム" + }, + "options": { + "display": "表示", + "row": "行", + "list": "リスト", + "image_style": "画像のスタイル", + "poster": "ポスター", + "cover": "カバー", + "show_titles": "タイトルの表示", + "show_stats": "統計を表示" + }, + "filters": { + "genres": "ジャンル", + "years": "年", + "sort_by": "ソート", + "sort_order": "ソート順", + "asc": "Ascending", + "desc": "Descending", + "tags": "タグ" + } + }, + "favorites": { + "series": "シリーズ", + "movies": "映画", + "episodes": "エピソード", + "videos": "ビデオ", + "boxsets": "ボックスセット", + "playlists": "プレイリスト", + "noDataTitle": "お気に入りはまだありません", + "noData": "アイテムをお気に入りとしてマークすると、ここに表示されクイックアクセスできるようになります。" + }, + "custom_links": { + "no_links": "リンクがありません" + }, + "player": { + "error": "エラー", + "failed_to_get_stream_url": "ストリームURLを取得できませんでした", + "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", + "client_error": "クライアントエラー", + "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", + "message_from_server": "サーバーからのメッセージ", + "video_has_finished_playing": "ビデオの再生が終了しました!", + "no_video_source": "動画ソースがありません...", + "next_episode": "次のエピソード", + "refresh_tracks": "トラックを更新", + "subtitle_tracks": "字幕トラック:", + "audio_tracks": "音声トラック:", + "playback_state": "再生状態:", + "no_data_available": "データなし", + "index": "インデックス:" + }, + "item_card": { + "next_up": "次", + "no_items_to_display": "表示するアイテムがありません", + "cast_and_crew": "キャスト&クルー", + "series": "シリーズ", + "seasons": "シーズン", + "season": "シーズン", + "no_episodes_for_this_season": "このシーズンのエピソードはありません", + "overview": "ストーリー", + "more_with": "{{name}}の詳細", + "similar_items": "類似アイテム", + "no_similar_items_found": "類似のアイテムは見つかりませんでした", + "video": "映像", + "more_details": "さらに詳細を表示", + "quality": "画質", + "audio": "音声", + "subtitles": "字幕", + "show_more": "もっと見る", + "show_less": "少なく表示", + "appeared_in": "出演作品", + "could_not_load_item": "アイテムを読み込めませんでした", + "none": "なし", + "download": { + "download_season": "シーズンをダウンロード", + "download_series": "シリーズをダウンロード", + "download_episode": "エピソードをダウンロード", + "download_movie": "映画をダウンロード", + "download_x_item": "{{item_count}}のアイテムをダウンロード", + "download_button": "ダウンロード", + "using_optimized_server": "Optimizeサーバーを使用する", + "using_default_method": "デフォルトの方法を使用" + } + }, + "live_tv": { + "next": "次", + "previous": "前", + "live_tv": "ライブTV", + "coming_soon": "近日公開", + "on_now": "現在", + "shows": "表示", + "movies": "映画", + "sports": "スポーツ", + "for_kids": "子供向け", + "news": "ニュース" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "キャンセル", + "yes": "はい", + "whats_wrong": "どうしましたか?", + "issue_type": "問題の種類", + "select_an_issue": "問題を選択", + "types": "種類", + "describe_the_issue": "(オプション) 問題を説明してください...", + "submit_button": "送信", + "report_issue_button": "チケットを報告", + "request_button": "リクエスト", + "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", + "failed_to_login": "ログインに失敗しました", + "cast": "出演者", + "details": "詳細", + "status": "状態", + "original_title": "原題", + "series_type": "シリーズタイプ", + "release_dates": "公開日", + "first_air_date": "初放送日", + "next_air_date": "次回放送日", + "revenue": "収益", + "budget": "予算", + "original_language": "オリジナルの言語", + "production_country": "制作国", + "studios": "制作会社", + "network": "ネットワーク", + "currently_streaming_on": "ストリーミング中", + "advanced": "詳細", + "request_as": "別ユーザーとしてリクエスト", + "tags": "タグ", + "quality_profile": "画質プロファイル", + "root_folder": "ルートフォルダ", + "season_all": "Season (all)", + "season_number": "シーズン{{season_number}}", + "number_episodes": "エピソード{{episode_number}}", + "born": "生まれ", + "appearances": "出演", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", + "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", + "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", + "issue_submitted": "チケットを送信しました!", + "requested_item": "{{item}}をリクエスト!", + "you_dont_have_permission_to_request": "リクエストする権限がありません!", + "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" + } + }, + "tabs": { + "home": "ホーム", + "search": "検索", + "library": "ライブラリ", + "custom_links": "カスタムリンク", + "favorites": "お気に入り" + } } diff --git a/translations/nl.json b/translations/nl.json index 54c39e34..039803cc 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -1,471 +1,473 @@ { - "login": { - "username_required": "Gebruikersnaam is verplicht", - "error_title": "Fout", - "login_title": "Aanmelden", - "login_to_title": "Aanmelden bij", - "username_placeholder": "Gebruikersnaam", - "password_placeholder": "Wachtwoord", - "login_button": "Aanmelden", - "quick_connect": "Snel Verbinden", - "enter_code_to_login": "Vul code {{code}} in om aan te melden", - "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", - "got_it": "Begrepen", - "connection_failed": "Verbinding mislukt", - "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", - "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", - "change_server": "Server wijzigen", - "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", - "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", - "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", - "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", - "there_is_a_server_error": "Er is een serverfout", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" - }, - "server": { - "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", - "server_url_placeholder": "http(s)://je-server.com", - "connect_button": "Verbinden", - "previous_servers": "vorige servers", - "clear_button": "Wissen", - "search_for_local_servers": "Zoek naar lokale servers", - "searching": "Zoeken...", - "servers": "Servers" - }, - "home": { - "no_internet": "Geen Internet", - "no_items": "Geen items", - "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", - "go_to_downloads": "Ga naar downloads", - "oops": "Oeps!", - "error_message": "Er ging iets fout\nGelieve af en aan te melden.", - "continue_watching": "Verder Kijken", - "next_up": "Volgende", - "recently_added_in": "Recent toegevoegd in {{libraryName}}", - "suggested_movies": "Voorgestelde films", - "suggested_episodes": "Voorgestelde Afleveringen", - "intro": { - "welcome_to_streamyfin": "Welkom bij Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", - "features_title": "Functies", - "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", - "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", - "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", - "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", - "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", - "done_button": "Gedaan", - "go_to_settings_button": "Ga naar instellingen", - "read_more": "Lees meer" - }, - "settings": { - "settings_title": "Instellingen", - "log_out_button": "Afmelden", - "user_info": { - "user_info_title": "Gebruiker Info", - "user": "Gebruiker", - "server": "Server", - "token": "Token", - "app_version": "App Versie" - }, - "quick_connect": { - "quick_connect_title": "Snel Verbinden", - "authorize_button": "Snel Verbinden toestaan", - "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", - "success": "Succes", - "quick_connect_autorized": "Snel Verbinden toegestaan", - "error": "Fout", - "invalid_code": "Ongeldige code", - "authorize": "Toestaan" - }, - "media_controls": { - "media_controls_title": "Media Bedieningen", - "forward_skip_length": "Duur voorwaarts overslaan", - "rewind_length": "Duur terugspoelen", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Gebruik Audio Track Van Vorig Item", - "audio_language": "Audio taal", - "audio_hint": "Kies een standaard audio taal.", - "none": "Geen", - "language": "Taal" - }, - "subtitles": { - "subtitle_title": "Ondertitels", - "subtitle_language": "Ondertitel taal", - "subtitle_mode": "Ondertitelmodus", - "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", - "subtitle_size": "Ondertitel Grootte", - "subtitle_hint": "Stel ondertitel voorkeuren in.", - "none": "Geen", - "language": "Taal", - "loading": "Laden", - "modes": { - "Default": "Standaard", - "Smart": "Slim", - "Always": "Altijd", - "None": "Geen", - "OnlyForced": "Alleen Geforceerd" - } - }, - "other": { - "other_title": "Andere", - "follow_device_orientation": "Automatisch draaien", - "video_orientation": "Video oriëntatie", - "orientation": "Oriëntatie", - "orientations": { - "DEFAULT": "Standaard", - "ALL": "Alle", - "PORTRAIT": "Portret", - "PORTRAIT_UP": "Portret Omhoog", - "PORTRAIT_DOWN": "Portret Omlaag", - "LANDSCAPE": "Landschap", - "LANDSCAPE_LEFT": "Landschap Links", - "LANDSCAPE_RIGHT": "Landschap Rechts", - "OTHER": "Andere", - "UNKNOWN": "Onbekend" - }, - "safe_area_in_controls": "Veilig gebied in bedieningen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Aangepaste menulinks tonen", - "hide_libraries": "Verberg Bibliotheken", - "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", - "disable_haptic_feedback": "Haptische feedback uitschakelen", - "default_quality": "Standaard kwaliteit" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download methode", - "remux_max_download": "Maximale Remux-download", - "auto_download": "Auto download", - "optimized_versions_server": "Geoptimaliseerde server versies", - "save_button": "Opslaan", - "optimized_server": "Geoptimaliseerde Server", - "optimized": "Geoptimaliseerd", - "default": "Standaard", - "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", - "server_url": "Server URL", - "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Wachtwoord", - "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", - "save_button": "Opslaan", - "clear_button": "Wissen", - "login_button": "Aanmelden", - "total_media_requests": "Totaal aantal mediaverzoeken", - "movie_quota_limit": "Limiet filmquota", - "movie_quota_days": "Filmquota dagen", - "tv_quota_limit": "Limiet serie quota", - "tv_quota_days": "Serie Quota dagen", - "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", - "unlimited": "Ongelimiteerd", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Search inschakelen ", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort", - "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_marlin": "Lees meer over Marlin.", - "save_button": "Opslaan", - "toasts": { - "saved": "Opgeslagen" - } - } - }, - "storage": { - "storage_title": "Opslag", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Toestel {{availableSpace}}%", - "size_used": "{{used}} van {{total}} gebruikt", - "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" - }, - "intro": { - "show_intro": "Toon intro", - "reset_intro": "intro opnieuw instellen" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Geen logs beschikbaar", - "delete_all_logs": "Verwijder alle logs" - }, - "languages": { - "title": "Talen", - "app_language": "App taal", - "app_language_description": "Selecteer een taal voor de app.", - "system": "Systeem" - }, - "toasts": { - "error_deleting_files": "Fout bij het verwijderen van bestanden", - "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", - "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", - "connected": "Verbonden", - "could_not_connect": "Kon niet verbinden", - "invalid_url": "Ongeldige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "Series", - "movies": "Films", - "queue": "Wachtrij", - "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", - "no_items_in_queue": "Geen items in wachtrij", - "no_downloaded_items": "Geen gedownloade items", - "delete_all_movies_button": "Verwijder alle films", - "delete_all_tvseries_button": "Verwijder alle Series", - "delete_all_button": "Verwijder alles", - "active_download": "Actieve download", - "no_active_downloads": "Geen actieve downloads", - "active_downloads": "Actieve downloads", - "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", - "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", - "back": "Terug", - "delete": "Verwijder", - "something_went_wrong": "Er ging iets mis", - "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", - "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", - "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", - "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", - "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", - "download_cancelled": "Download geannuleerd", - "could_not_cancel_download": "Kon de download niet annuleren", - "download_completed": "Download afgerond", - "download_started_for": "Download gestart voor {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", - "download_stated_for_item": "Download gestart voor {{item}}", - "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", - "download_completed_for_item": "Download afgerond voor {{item}}", - "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", - "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", - "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", - "no_response_received_from_server": "Geen antwoord gekregen van de server", - "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", - "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", - "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", - "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", - "go_to_downloads": "Ga naar downloads" - } - } - }, - "search": { - "search_here": "Zoek hier...", - "search": "Zoek...", - "x_items": "{{count}} items", - "library": "Bibliotheek", - "discover": "Ontdek", - "no_results": "Geen resultaten", - "no_results_found_for": "Geen resultaten gevonden voor", - "movies": "Films", - "series": "Series", - "episodes": "Afleveringen", - "collections": "Collecties", - "actors": "Acteurs", - "request_movies": "Vraag films aan", - "request_series": "Vraag series aan", - "recently_added": "Recent Toegevoegd", - "recent_requests": "Recent Aangevraagd", - "plex_watchlist": "Plex Kijklijst", - "trending": "Trending", - "popular_movies": "Populaire films", - "movie_genres": "Film Genres", - "upcoming_movies": "Aankomende films", - "studios": "Studios", - "popular_tv": "Populaire TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Aankomende TV", - "networks": "Netwerken", - "tmdb_movie_keyword": "TMDB Film Trefwoord", - "tmdb_movie_genre": "TMDB Filmgenres", - "tmdb_tv_keyword": "TMDB TV Trefwoord", - "tmdb_tv_genre": "TMDB TV-Genres", - "tmdb_search": "TMDB Zoeken", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netwerk", - "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", - "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" - }, - "library": { - "no_items_found": "Geen items gevonden", - "no_results": "Geen resultaten", - "no_libraries_found": "Geen bibliotheken gevonden", - "item_types": { - "movies": "Films", - "series": "Series", - "boxsets": "Boxsets", - "items": "items" - }, - "options": { - "display": "Weergave", - "row": "Rij", - "list": "Lijst", - "image_style": "Stijl van afbeelding", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Toon titels", - "show_stats": "Toon statistieken" - }, - "filters": { - "genres": "Genres", - "years": "Jaren", - "sort_by": "Sorteren op", - "sort_order": "Sorteer volgorde", - "asc": "Ascending", - "desc": "Descending", - "tags": "Labels" - } - }, - "favorites": { - "series": "Series", - "movies": "Films", - "episodes": "Afleveringen", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Afspeellijsten" - }, - "custom_links": { - "no_links": "Geen links" - }, - "player": { - "error": "Fout", - "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", - "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", - "client_error": "Fout van de client", - "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", - "message_from_server": "Bericht van de server", - "video_has_finished_playing": "Video is gedaan met spelen!", - "no_video_source": "Geen videobron...", - "next_episode": "Volgende Aflevering", - "refresh_tracks": "Tracks verversen", - "subtitle_tracks": "Ondertitel Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Afspeelstatus:", - "no_data_available": "Geen data beschikbaar", - "index": "Index:" - }, - "item_card": { - "next_up": "Volgende", - "no_items_to_display": "Geen items om te tonen", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seizoenen", - "season": "Seizoen", - "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", - "overview": "Overzicht", - "more_with": "Meer met {{name}}", - "similar_items": "Gelijkaardige items", - "no_similar_items_found": "Geen gelijkaardige items gevonden", - "video": "Video", - "more_details": "Meer details", - "quality": "Kwaliteit", - "audio": "Audio", - "subtitles": "Ondertitel", - "show_more": "Toon meer", - "show_less": "Toon minder", - "appeared_in": "Verschenen in", - "could_not_load_item": "Kon item niet laden", - "none": "Geen", - "download": { - "download_season": "Download Seizoen", - "download_series": "Download Serie", - "download_episode": "Download Aflevering", - "download_movie": "Download Film", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Geoptimaliseerde server gebruiken", - "using_default_method": "Standaard methode gebruiken" - } - }, - "live_tv": { - "next": "Volgende ", - "previous": "Vorige", - "live_tv": "Live TV", - "coming_soon": "Binnenkort beschikbaar", - "on_now": "Nu op", - "shows": "Shows", - "movies": "Films", - "sports": "Sport", - "for_kids": "Voor kinderen", - "news": "Nieuws" - }, - "jellyseerr": { - "confirm": "Bevestig", - "cancel": "Annuleer", - "yes": "Ja", - "whats_wrong": "Wat is er mis?", - "issue_type": "Type probleem", - "select_an_issue": "Selecteer een probleem", - "types": "Types", - "describe_the_issue": "(optioneel) beschrijf het probleem...", - "submit_button": "Verzenden", - "report_issue_button": "Meld een probleem", - "request_button": "Aanvragen", - "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", - "failed_to_login": "Kon niet aanmelden", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Originele titel", - "series_type": "Serietype", - "release_dates": "Verschijningsdatums", - "first_air_date": "Eerste uitzenddatum", - "next_air_date": "Volgende uitzenddatum", - "revenue": "Inkomsten", - "budget": "Budget", - "original_language": "Originele taal", - "production_country": "Land van productie", - "studios": "Studio", - "network": "Netwerk", - "currently_streaming_on": "Momenteel te streamen op", - "advanced": "Geavanceerd", - "request_as": "Vraag aan als", - "tags": "Labels", - "quality_profile": "Kwaliteitsprofiel", - "root_folder": "Hoofdmap", - "season_all": "Season (all)", - "season_number": "Seizoen {{season_number}}", - "number_episodes": "{{episode_number}} Afleveringen", - "born": "Geboren", - "appearances": "Verschijningen", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", - "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", - "issue_submitted": "Probleem ingediend!", - "requested_item": "{{item}} aangevraagd!", - "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", - "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" - } - }, - "tabs": { - "home": "Thuis", - "search": "Zoeken", - "library": "Bibliotheek", - "custom_links": "Aangepaste links", - "favorites": "Favorieten" - } + "login": { + "username_required": "Gebruikersnaam is verplicht", + "error_title": "Fout", + "login_title": "Aanmelden", + "login_to_title": "Aanmelden bij", + "username_placeholder": "Gebruikersnaam", + "password_placeholder": "Wachtwoord", + "login_button": "Aanmelden", + "quick_connect": "Snel Verbinden", + "enter_code_to_login": "Vul code {{code}} in om aan te melden", + "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", + "got_it": "Begrepen", + "connection_failed": "Verbinding mislukt", + "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", + "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", + "change_server": "Server wijzigen", + "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", + "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", + "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", + "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", + "there_is_a_server_error": "Er is een serverfout", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" + }, + "server": { + "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", + "server_url_placeholder": "http(s)://je-server.com", + "connect_button": "Verbinden", + "previous_servers": "vorige servers", + "clear_button": "Wissen", + "search_for_local_servers": "Zoek naar lokale servers", + "searching": "Zoeken...", + "servers": "Servers" + }, + "home": { + "no_internet": "Geen Internet", + "no_items": "Geen items", + "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", + "go_to_downloads": "Ga naar downloads", + "oops": "Oeps!", + "error_message": "Er ging iets fout\nGelieve af en aan te melden.", + "continue_watching": "Verder Kijken", + "next_up": "Volgende", + "recently_added_in": "Recent toegevoegd in {{libraryName}}", + "suggested_movies": "Voorgestelde films", + "suggested_episodes": "Voorgestelde Afleveringen", + "intro": { + "welcome_to_streamyfin": "Welkom bij Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", + "features_title": "Functies", + "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", + "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", + "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", + "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", + "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", + "done_button": "Gedaan", + "go_to_settings_button": "Ga naar instellingen", + "read_more": "Lees meer" + }, + "settings": { + "settings_title": "Instellingen", + "log_out_button": "Afmelden", + "user_info": { + "user_info_title": "Gebruiker Info", + "user": "Gebruiker", + "server": "Server", + "token": "Token", + "app_version": "App Versie" + }, + "quick_connect": { + "quick_connect_title": "Snel Verbinden", + "authorize_button": "Snel Verbinden toestaan", + "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", + "success": "Succes", + "quick_connect_autorized": "Snel Verbinden toegestaan", + "error": "Fout", + "invalid_code": "Ongeldige code", + "authorize": "Toestaan" + }, + "media_controls": { + "media_controls_title": "Media Bedieningen", + "forward_skip_length": "Duur voorwaarts overslaan", + "rewind_length": "Duur terugspoelen", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Gebruik Audio Track Van Vorig Item", + "audio_language": "Audio taal", + "audio_hint": "Kies een standaard audio taal.", + "none": "Geen", + "language": "Taal" + }, + "subtitles": { + "subtitle_title": "Ondertitels", + "subtitle_language": "Ondertitel taal", + "subtitle_mode": "Ondertitelmodus", + "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", + "subtitle_size": "Ondertitel Grootte", + "subtitle_hint": "Stel ondertitel voorkeuren in.", + "none": "Geen", + "language": "Taal", + "loading": "Laden", + "modes": { + "Default": "Standaard", + "Smart": "Slim", + "Always": "Altijd", + "None": "Geen", + "OnlyForced": "Alleen Geforceerd" + } + }, + "other": { + "other_title": "Andere", + "follow_device_orientation": "Automatisch draaien", + "video_orientation": "Video oriëntatie", + "orientation": "Oriëntatie", + "orientations": { + "DEFAULT": "Standaard", + "ALL": "Alle", + "PORTRAIT": "Portret", + "PORTRAIT_UP": "Portret Omhoog", + "PORTRAIT_DOWN": "Portret Omlaag", + "LANDSCAPE": "Landschap", + "LANDSCAPE_LEFT": "Landschap Links", + "LANDSCAPE_RIGHT": "Landschap Rechts", + "OTHER": "Andere", + "UNKNOWN": "Onbekend" + }, + "safe_area_in_controls": "Veilig gebied in bedieningen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Aangepaste menulinks tonen", + "hide_libraries": "Verberg Bibliotheken", + "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", + "disable_haptic_feedback": "Haptische feedback uitschakelen", + "default_quality": "Standaard kwaliteit" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download methode", + "remux_max_download": "Maximale Remux-download", + "auto_download": "Auto download", + "optimized_versions_server": "Geoptimaliseerde server versies", + "save_button": "Opslaan", + "optimized_server": "Geoptimaliseerde Server", + "optimized": "Geoptimaliseerd", + "default": "Standaard", + "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", + "server_url": "Server URL", + "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Wachtwoord", + "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", + "save_button": "Opslaan", + "clear_button": "Wissen", + "login_button": "Aanmelden", + "total_media_requests": "Totaal aantal mediaverzoeken", + "movie_quota_limit": "Limiet filmquota", + "movie_quota_days": "Filmquota dagen", + "tv_quota_limit": "Limiet serie quota", + "tv_quota_days": "Serie Quota dagen", + "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", + "unlimited": "Ongelimiteerd", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Search inschakelen ", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort", + "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_marlin": "Lees meer over Marlin.", + "save_button": "Opslaan", + "toasts": { + "saved": "Opgeslagen" + } + } + }, + "storage": { + "storage_title": "Opslag", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Toestel {{availableSpace}}%", + "size_used": "{{used}} van {{total}} gebruikt", + "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" + }, + "intro": { + "show_intro": "Toon intro", + "reset_intro": "intro opnieuw instellen" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Geen logs beschikbaar", + "delete_all_logs": "Verwijder alle logs" + }, + "languages": { + "title": "Talen", + "app_language": "App taal", + "app_language_description": "Selecteer een taal voor de app.", + "system": "Systeem" + }, + "toasts": { + "error_deleting_files": "Fout bij het verwijderen van bestanden", + "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", + "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", + "connected": "Verbonden", + "could_not_connect": "Kon niet verbinden", + "invalid_url": "Ongeldige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "Series", + "movies": "Films", + "queue": "Wachtrij", + "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", + "no_items_in_queue": "Geen items in wachtrij", + "no_downloaded_items": "Geen gedownloade items", + "delete_all_movies_button": "Verwijder alle films", + "delete_all_tvseries_button": "Verwijder alle Series", + "delete_all_button": "Verwijder alles", + "active_download": "Actieve download", + "no_active_downloads": "Geen actieve downloads", + "active_downloads": "Actieve downloads", + "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", + "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", + "back": "Terug", + "delete": "Verwijder", + "something_went_wrong": "Er ging iets mis", + "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", + "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", + "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", + "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", + "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", + "download_cancelled": "Download geannuleerd", + "could_not_cancel_download": "Kon de download niet annuleren", + "download_completed": "Download afgerond", + "download_started_for": "Download gestart voor {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", + "download_stated_for_item": "Download gestart voor {{item}}", + "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", + "download_completed_for_item": "Download afgerond voor {{item}}", + "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", + "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", + "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", + "no_response_received_from_server": "Geen antwoord gekregen van de server", + "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", + "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", + "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", + "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", + "go_to_downloads": "Ga naar downloads" + } + } + }, + "search": { + "search_here": "Zoek hier...", + "search": "Zoek...", + "x_items": "{{count}} items", + "library": "Bibliotheek", + "discover": "Ontdek", + "no_results": "Geen resultaten", + "no_results_found_for": "Geen resultaten gevonden voor", + "movies": "Films", + "series": "Series", + "episodes": "Afleveringen", + "collections": "Collecties", + "actors": "Acteurs", + "request_movies": "Vraag films aan", + "request_series": "Vraag series aan", + "recently_added": "Recent Toegevoegd", + "recent_requests": "Recent Aangevraagd", + "plex_watchlist": "Plex Kijklijst", + "trending": "Trending", + "popular_movies": "Populaire films", + "movie_genres": "Film Genres", + "upcoming_movies": "Aankomende films", + "studios": "Studios", + "popular_tv": "Populaire TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Aankomende TV", + "networks": "Netwerken", + "tmdb_movie_keyword": "TMDB Film Trefwoord", + "tmdb_movie_genre": "TMDB Filmgenres", + "tmdb_tv_keyword": "TMDB TV Trefwoord", + "tmdb_tv_genre": "TMDB TV-Genres", + "tmdb_search": "TMDB Zoeken", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netwerk", + "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", + "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" + }, + "library": { + "no_items_found": "Geen items gevonden", + "no_results": "Geen resultaten", + "no_libraries_found": "Geen bibliotheken gevonden", + "item_types": { + "movies": "Films", + "series": "Series", + "boxsets": "Boxsets", + "items": "items" + }, + "options": { + "display": "Weergave", + "row": "Rij", + "list": "Lijst", + "image_style": "Stijl van afbeelding", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Toon titels", + "show_stats": "Toon statistieken" + }, + "filters": { + "genres": "Genres", + "years": "Jaren", + "sort_by": "Sorteren op", + "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", + "tags": "Labels" + } + }, + "favorites": { + "series": "Series", + "movies": "Films", + "episodes": "Afleveringen", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Afspeellijsten", + "noDataTitle": "Nog geen favorieten", + "noData": "Markeer items als favoriet om ze hier te laten verschijnen voor snelle toegang." + }, + "custom_links": { + "no_links": "Geen links" + }, + "player": { + "error": "Fout", + "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", + "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", + "client_error": "Fout van de client", + "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", + "message_from_server": "Bericht van de server", + "video_has_finished_playing": "Video is gedaan met spelen!", + "no_video_source": "Geen videobron...", + "next_episode": "Volgende Aflevering", + "refresh_tracks": "Tracks verversen", + "subtitle_tracks": "Ondertitel Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Afspeelstatus:", + "no_data_available": "Geen data beschikbaar", + "index": "Index:" + }, + "item_card": { + "next_up": "Volgende", + "no_items_to_display": "Geen items om te tonen", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seizoenen", + "season": "Seizoen", + "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", + "overview": "Overzicht", + "more_with": "Meer met {{name}}", + "similar_items": "Gelijkaardige items", + "no_similar_items_found": "Geen gelijkaardige items gevonden", + "video": "Video", + "more_details": "Meer details", + "quality": "Kwaliteit", + "audio": "Audio", + "subtitles": "Ondertitel", + "show_more": "Toon meer", + "show_less": "Toon minder", + "appeared_in": "Verschenen in", + "could_not_load_item": "Kon item niet laden", + "none": "Geen", + "download": { + "download_season": "Download Seizoen", + "download_series": "Download Serie", + "download_episode": "Download Aflevering", + "download_movie": "Download Film", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Geoptimaliseerde server gebruiken", + "using_default_method": "Standaard methode gebruiken" + } + }, + "live_tv": { + "next": "Volgende ", + "previous": "Vorige", + "live_tv": "Live TV", + "coming_soon": "Binnenkort beschikbaar", + "on_now": "Nu op", + "shows": "Shows", + "movies": "Films", + "sports": "Sport", + "for_kids": "Voor kinderen", + "news": "Nieuws" + }, + "jellyseerr": { + "confirm": "Bevestig", + "cancel": "Annuleer", + "yes": "Ja", + "whats_wrong": "Wat is er mis?", + "issue_type": "Type probleem", + "select_an_issue": "Selecteer een probleem", + "types": "Types", + "describe_the_issue": "(optioneel) beschrijf het probleem...", + "submit_button": "Verzenden", + "report_issue_button": "Meld een probleem", + "request_button": "Aanvragen", + "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", + "failed_to_login": "Kon niet aanmelden", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Originele titel", + "series_type": "Serietype", + "release_dates": "Verschijningsdatums", + "first_air_date": "Eerste uitzenddatum", + "next_air_date": "Volgende uitzenddatum", + "revenue": "Inkomsten", + "budget": "Budget", + "original_language": "Originele taal", + "production_country": "Land van productie", + "studios": "Studio", + "network": "Netwerk", + "currently_streaming_on": "Momenteel te streamen op", + "advanced": "Geavanceerd", + "request_as": "Vraag aan als", + "tags": "Labels", + "quality_profile": "Kwaliteitsprofiel", + "root_folder": "Hoofdmap", + "season_all": "Season (all)", + "season_number": "Seizoen {{season_number}}", + "number_episodes": "{{episode_number}} Afleveringen", + "born": "Geboren", + "appearances": "Verschijningen", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", + "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", + "issue_submitted": "Probleem ingediend!", + "requested_item": "{{item}} aangevraagd!", + "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", + "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" + } + }, + "tabs": { + "home": "Thuis", + "search": "Zoeken", + "library": "Bibliotheek", + "custom_links": "Aangepaste links", + "favorites": "Favorieten" + } } diff --git a/translations/sv.json b/translations/sv.json index d35f6c82..c5a10124 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -1,30 +1,34 @@ { - "login": { - "username_required": "Användarnamn krävs", - "error_title": "Fel", - "login_title": "Logga in", - "username_placeholder": "Användarnamn", - "password_placeholder": "Lösenord", - "login_button": "Logga in" - }, - "server": { - "server_url_placeholder": "Server URL", - "connect_button": "Anslut" - }, - "home": { - "home": "Hem", - "no_internet": "Ingen Internet", - "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", - "go_to_downloads": "Gå till nedladdningar", - "oops": "Hoppsan!", - "error_message": "Något gick fel.\nLogga ut och in igen.", - "continue_watching": "Fortsätt titta", - "next_up": "Nästa upp", - "recently_added_in": "Nyligen tillagt i {{libraryName}}" - }, - "tabs": { - "home": "Hem", - "search": "Sök", - "library": "Bibliotek" - } + "login": { + "username_required": "Användarnamn krävs", + "error_title": "Fel", + "login_title": "Logga in", + "username_placeholder": "Användarnamn", + "password_placeholder": "Lösenord", + "login_button": "Logga in" + }, + "server": { + "server_url_placeholder": "Server URL", + "connect_button": "Anslut" + }, + "home": { + "home": "Hem", + "no_internet": "Ingen Internet", + "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", + "go_to_downloads": "Gå till nedladdningar", + "oops": "Hoppsan!", + "error_message": "Något gick fel.\nLogga ut och in igen.", + "continue_watching": "Fortsätt titta", + "next_up": "Nästa upp", + "recently_added_in": "Nyligen tillagt i {{libraryName}}" + }, + "favorites": { + "noDataTitle": "Inga favoriter än", + "noData": "Markera objekt som favoriter för att se dem visas här för snabb åtkomst." + }, + "tabs": { + "home": "Hem", + "search": "Sök", + "library": "Bibliotek" + } } diff --git a/translations/tr.json b/translations/tr.json index f9e0e45f..7886423c 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -1,470 +1,472 @@ { - "login": { - "username_required": "Kullanıcı adı gereklidir", - "error_title": "Hata", - "login_title": "Giriş yap", - "login_to_title": " 'e giriş yap", - "username_placeholder": "Kullanıcı adı", - "password_placeholder": "Şifre", - "login_button": "Giriş yap", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", - "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", - "got_it": "Anlaşıldı", - "connection_failed": "Bağlantı başarısız", - "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", - "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", - "change_server": "Sunucuyu değiştir", - "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", - "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", - "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", - "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", - "there_is_a_server_error": "Sunucu hatası var", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" - }, - "server": { - "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", - "server_url_placeholder": "http(s)://sunucunuz.com", - "connect_button": "Bağlan", - "previous_servers": "Önceki sunucular", - "clear_button": "Temizle", - "search_for_local_servers": "Yerel sunucuları ara", - "searching": "Aranıyor...", - "servers": "Sunucular" - }, - "home": { - "no_internet": "İnternet Yok", - "no_items": "Öge Yok", - "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", - "go_to_downloads": "İndirmelere Git", - "oops": "Hups!", - "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", - "continue_watching": "İzlemeye Devam Et", - "next_up": "Sonraki", - "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", - "suggested_movies": "Önerilen Filmler", - "suggested_episodes": "Önerilen Bölümler", - "intro": { - "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", - "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", - "features_title": "Özellikler", - "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", - "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", - "downloads_feature_title": "İndirmeler", - "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", - "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", - "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", - "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", - "done_button": "Tamam", - "go_to_settings_button": "Ayrıntılara Git", - "read_more": "Daha fazla oku" - }, - "settings": { - "settings_title": "Ayarlar", - "log_out_button": "Çıkış Yap", - "user_info": { - "user_info_title": "Kullanıcı Bilgisi", - "user": "Kullanıcı", - "server": "Sunucu", - "token": "Token", - "app_version": "Uygulama Sürümü" - }, - "quick_connect": { - "quick_connect_title": "Hızlı Bağlantı", - "authorize_button": "Hızlı Bağlantıyı Yetkilendir", - "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", - "success": "Başarılı", - "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", - "error": "Hata", - "invalid_code": "Geçersiz kod", - "authorize": "Yetkilendir" - }, - "media_controls": { - "media_controls_title": "Medya Kontrolleri", - "forward_skip_length": "İleri Sarma Uzunluğu", - "rewind_length": "Geri Sarma Uzunluğu", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Ses", - "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", - "audio_language": "Ses Dili", - "audio_hint": "Varsayılan ses dilini seçin.", - "none": "Yok", - "language": "Dil" - }, - "subtitles": { - "subtitle_title": "Altyazılar", - "subtitle_language": "Altyazı Dili", - "subtitle_mode": "Altyazı Modu", - "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", - "subtitle_size": "Altyazı Boyutu", - "subtitle_hint": "Altyazı tercihini yapılandırın.", - "none": "Yok", - "language": "Dil", - "loading": "Yükleniyor", - "modes": { - "Default": "Varsayılan", - "Smart": "Akıllı", - "Always": "Her Zaman", - "None": "Yok", - "OnlyForced": "Sadece Zorunlu" - } - }, - "other": { - "other_title": "Diğer", - "follow_device_orientation": "Otomatik Döndürme", - "video_orientation": "Video Yönü", - "orientation": "Yön", - "orientations": { - "DEFAULT": "Varsayılan", - "ALL": "Tümü", - "PORTRAIT": "Dikey", - "PORTRAIT_UP": "Dikey Yukarı", - "PORTRAIT_DOWN": "Dikey Aşağı", - "LANDSCAPE": "Yatay", - "LANDSCAPE_LEFT": "Yatay Sol", - "LANDSCAPE_RIGHT": "Yatay Sağ", - "OTHER": "Diğer", - "UNKNOWN": "Bilinmeyen" - }, - "safe_area_in_controls": "Kontrollerde Güvenli Alan", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", - "hide_libraries": "Kütüphaneleri Gizle", - "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", - "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" - }, - "downloads": { - "downloads_title": "İndirmeler", - "download_method": "İndirme Yöntemi", - "remux_max_download": "Remux max indirme", - "auto_download": "Otomatik İndirme", - "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", - "save_button": "Kaydet", - "optimized_server": "Optimize Sunucu", - "optimized": "Optimize", - "default": "Varsayılan", - "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Eklentiler", - "jellyseerr": { - "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", - "server_url": "Sunucu URL'si", - "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Şifre", - "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", - "save_button": "Kaydet", - "clear_button": "Temizle", - "login_button": "Giriş Yap", - "total_media_requests": "Toplam medya istekleri", - "movie_quota_limit": "Film kota limiti", - "movie_quota_days": "Film kota günleri", - "tv_quota_limit": "TV kota limiti", - "tv_quota_days": "TV kota günleri", - "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", - "unlimited": "Sınırsız", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Aramasını Etkinleştir ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_marlin": "Marlin hakkında daha fazla oku.", - "save_button": "Kaydet", - "toasts": { - "saved": "Kaydedildi" - } - } - }, - "storage": { - "storage_title": "Depolama", - "app_usage": "Uygulama {{usedSpace}}%", - "device_usage": "Cihaz {{availableSpace}}%", - "size_used": "{{used}} / {{total}} kullanıldı", - "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" - }, - "intro": { - "show_intro": "Tanıtımı Göster", - "reset_intro": "Tanıtımı Sıfırla" - }, - "logs": { - "logs_title": "Günlükler", - "no_logs_available": "Günlükler mevcut değil", - "delete_all_logs": "Tüm günlükleri sil" - }, - "languages": { - "title": "Diller", - "app_language": "Uygulama dili", - "app_language_description": "Uygulama dilini seçin.", - "system": "Sistem" - }, - "toasts": { - "error_deleting_files": "Dosyalar silinirken hata oluştu", - "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", - "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", - "connected": "Bağlandı", - "could_not_connect": "Bağlanılamadı", - "invalid_url": "Geçersiz URL" - } - }, - "downloads": { - "downloads_title": "İndirilenler", - "tvseries": "Diziler", - "movies": "Filmler", - "queue": "Sıra", - "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", - "no_items_in_queue": "Sırada öğe yok", - "no_downloaded_items": "İndirilen öğe yok", - "delete_all_movies_button": "Tüm Filmleri Sil", - "delete_all_tvseries_button": "Tüm Dizileri Sil", - "delete_all_button": "Tümünü Sil", - "active_download": "Aktif indirme", - "no_active_downloads": "Aktif indirme yok", - "active_downloads": "Aktif indirmeler", - "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", - "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", - "back": "Geri", - "delete": "Sil", - "something_went_wrong": "Bir şeyler ters gitti", - "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", - "eta": "Tahmini Süre {{eta}}", - "methods": "Yöntemler", - "toasts": { - "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", - "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", - "failed_to_delete_all_movies": "Filmler silinemedi", - "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", - "failed_to_delete_all_tvseries": "Diziler silinemedi", - "download_cancelled": "İndirme iptal edildi", - "could_not_cancel_download": "İndirme iptal edilemedi", - "download_completed": "İndirme tamamlandı", - "download_started_for": "{{item}} için indirme başlatıldı", - "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", - "download_stated_for_item": "{{item}} için indirme başlatıldı", - "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", - "download_completed_for_item": "{{item}} için indirme tamamlandı", - "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", - "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", - "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", - "no_response_received_from_server": "Sunucudan yanıt alınamadı", - "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", - "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", - "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", - "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", - "go_to_downloads": "İndirmelere git" - } - } - }, - "search": { - "search_here": "Burada ara...", - "search": "Ara...", - "x_items": "{{count}} öge(ler)", - "library": "Kütüphane", - "discover": "Keşfet", - "no_results": "Sonuç bulunamadı", - "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", - "movies": "Filmler", - "series": "Diziler", - "episodes": "Bölümler", - "collections": "Koleksiyonlar", - "actors": "Oyuncular", - "request_movies": "Film Talep Et", - "request_series": "Dizi Talep Et", - "recently_added": "Son Eklenenler", - "recent_requests": "Son Talepler", - "plex_watchlist": "Plex İzleme Listesi", - "trending": "Şu An Popüler", - "popular_movies": "Popüler Filmler", - "movie_genres": "Film Türleri", - "upcoming_movies": "Yaklaşan Filmler", - "studios": "Stüdyolar", - "popular_tv": "Popüler Diziler", - "tv_genres": "Dizi Türleri", - "upcoming_tv": "Yaklaşan Diziler", - "networks": "Ağlar", - "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", - "tmdb_movie_genre": "TMDB Film Türü", - "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", - "tmdb_tv_genre": "TMDB Dizi Türü", - "tmdb_search": "TMDB Arama", - "tmdb_studio": "TMDB Stüdyo", - "tmdb_network": "TMDB Ağ", - "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", - "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" - }, - "library": { - "no_items_found": "Öğe bulunamadı", - "no_results": "Sonuç bulunamadı", - "no_libraries_found": "Kütüphane bulunamadı", - "item_types": { - "movies": "filmler", - "series": "diziler", - "boxsets": "koleksiyonlar", - "items": "ögeler" - }, - "options": { - "display": "Görüntüleme", - "row": "Satır", - "list": "Liste", - "image_style": "Görsel stili", - "poster": "Poster", - "cover": "Kapak", - "show_titles": "Başlıkları göster", - "show_stats": "İstatistikleri göster" - }, - "filters": { - "genres": "Türler", - "years": "Yıllar", - "sort_by": "Sırala", - "sort_order": "Sıralama düzeni", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiketler" - } - }, - "favorites": { - "series": "Diziler", - "movies": "Filmler", - "episodes": "Bölümler", - "videos": "Videolar", - "boxsets": "Koleksiyonlar", - "playlists": "Çalma listeleri" - }, - "custom_links": { - "no_links": "Bağlantı yok" - }, - "player": { - "error": "Hata", - "failed_to_get_stream_url": "Yayın URL'si alınamadı", - "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", - "client_error": "İstemci hatası", - "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", - "message_from_server": "Sunucudan mesaj: {{message}}", - "video_has_finished_playing": "Video oynatıldı!", - "no_video_source": "Video kaynağı yok...", - "next_episode": "Sonraki bölüm", - "refresh_tracks": "Parçaları yenile", - "subtitle_tracks": "Altyazı Parçaları:", - "audio_tracks": "Ses Parçaları:", - "playback_state": "Oynatma Durumu:", - "no_data_available": "Veri bulunamadı", - "index": "İndeks:" - }, - "item_card": { - "next_up": "Sıradaki", - "no_items_to_display": "Görüntülenecek öğe yok", - "cast_and_crew": "Oyuncular & Ekip", - "series": "Dizi", - "seasons": "Sezonlar", - "season": "Sezon", - "no_episodes_for_this_season": "Bu sezona ait bölüm yok", - "overview": "Özet", - "more_with": "Daha fazla {{name}}", - "similar_items": "Benzer ögeler", - "no_similar_items_found": "Benzer öge bulunamadı", - "video": "Video", - "more_details": "Daha fazla detay", - "quality": "Kalite", - "audio": "Ses", - "subtitles": "Altyazı", - "show_more": "Daha fazla göster", - "show_less": "Daha az göster", - "appeared_in": "Şurada yer aldı", - "could_not_load_item": "Öge yüklenemedi", - "none": "Hiçbiri", - "download": { - "download_season": "Sezonu indir", - "download_series": "Diziyi indir", - "download_episode": "Bölümü indir", - "download_movie": "Filmi indir", - "download_x_item": "{{item_count}} tane ögeyi indir", - "download_button": "İndir", - "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", - "using_default_method": "Varsayılan yöntem kullanılıyor" - } - }, - "live_tv": { - "next": "Sonraki", - "previous": "Önceki", - "live_tv": "Canlı TV", - "coming_soon": "Yakında", - "on_now": "Şu anda yayında", - "shows": "Programlar", - "movies": "Filmler", - "sports": "Spor", - "for_kids": "Çocuklar İçin", - "news": "Haberler" - }, - "jellyseerr": { - "confirm": "Onayla", - "cancel": "İptal", - "yes": "Evet", - "whats_wrong": "Problem nedir?", - "issue_type": "Sorun türü", - "select_an_issue": "Bir sorun seçin", - "types": "Türler", - "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", - "submit_button": "Gönder", - "report_issue_button": "Sorunu bildir", - "request_button": "Talep et", - "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", - "failed_to_login": "Giriş yapılamadı", - "cast": "Oyuncular", - "details": "Detaylar", - "status": "Durum", - "original_title": "Orijinal Başlık", - "series_type": "Dizi Türü", - "release_dates": "Yayın Tarihleri", - "first_air_date": "İlk Yayın Tarihi", - "next_air_date": "Sonraki Yayın Tarihi", - "revenue": "Gelir", - "budget": "Bütçe", - "original_language": "Orijinal Dil", - "production_country": "Yapım Ülkesi", - "studios": "Stüdyolar", - "network": "Ağ", - "currently_streaming_on": "Şu anda yayınlanıyor", - "advanced": "Gelişmiş", - "request_as": "Şu olarak iste", - "tags": "Etiketler", - "quality_profile": "Kalite Profili", - "root_folder": "Kök Klasör", - "season_all": "Season (all)", - "season_number": "Sezon {{season_number}}", - "number_episodes": "Bölüm {{episode_number}}", - "born": "Doğum", - "appearances": "Görünmeler", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", - "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", - "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", - "issue_submitted": "Sorun gönderildi!", - "requested_item": "{{item}} talep edildi!", - "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", - "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" - } - }, - "tabs": { - "home": "Ana Sayfa", - "search": "Ara", - "library": "Kütüphane", - "custom_links": "Özel Bağlantılar", - "favorites": "Favoriler" - } + "login": { + "username_required": "Kullanıcı adı gereklidir", + "error_title": "Hata", + "login_title": "Giriş yap", + "login_to_title": " 'e giriş yap", + "username_placeholder": "Kullanıcı adı", + "password_placeholder": "Şifre", + "login_button": "Giriş yap", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", + "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", + "got_it": "Anlaşıldı", + "connection_failed": "Bağlantı başarısız", + "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", + "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", + "change_server": "Sunucuyu değiştir", + "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", + "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", + "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", + "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", + "there_is_a_server_error": "Sunucu hatası var", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" + }, + "server": { + "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", + "server_url_placeholder": "http(s)://sunucunuz.com", + "connect_button": "Bağlan", + "previous_servers": "Önceki sunucular", + "clear_button": "Temizle", + "search_for_local_servers": "Yerel sunucuları ara", + "searching": "Aranıyor...", + "servers": "Sunucular" + }, + "home": { + "no_internet": "İnternet Yok", + "no_items": "Öge Yok", + "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", + "go_to_downloads": "İndirmelere Git", + "oops": "Hups!", + "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", + "continue_watching": "İzlemeye Devam Et", + "next_up": "Sonraki", + "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", + "suggested_movies": "Önerilen Filmler", + "suggested_episodes": "Önerilen Bölümler", + "intro": { + "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", + "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", + "features_title": "Özellikler", + "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", + "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", + "downloads_feature_title": "İndirmeler", + "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", + "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", + "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", + "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", + "done_button": "Tamam", + "go_to_settings_button": "Ayrıntılara Git", + "read_more": "Daha fazla oku" + }, + "settings": { + "settings_title": "Ayarlar", + "log_out_button": "Çıkış Yap", + "user_info": { + "user_info_title": "Kullanıcı Bilgisi", + "user": "Kullanıcı", + "server": "Sunucu", + "token": "Token", + "app_version": "Uygulama Sürümü" + }, + "quick_connect": { + "quick_connect_title": "Hızlı Bağlantı", + "authorize_button": "Hızlı Bağlantıyı Yetkilendir", + "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", + "success": "Başarılı", + "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", + "error": "Hata", + "invalid_code": "Geçersiz kod", + "authorize": "Yetkilendir" + }, + "media_controls": { + "media_controls_title": "Medya Kontrolleri", + "forward_skip_length": "İleri Sarma Uzunluğu", + "rewind_length": "Geri Sarma Uzunluğu", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Ses", + "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", + "audio_language": "Ses Dili", + "audio_hint": "Varsayılan ses dilini seçin.", + "none": "Yok", + "language": "Dil" + }, + "subtitles": { + "subtitle_title": "Altyazılar", + "subtitle_language": "Altyazı Dili", + "subtitle_mode": "Altyazı Modu", + "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", + "subtitle_size": "Altyazı Boyutu", + "subtitle_hint": "Altyazı tercihini yapılandırın.", + "none": "Yok", + "language": "Dil", + "loading": "Yükleniyor", + "modes": { + "Default": "Varsayılan", + "Smart": "Akıllı", + "Always": "Her Zaman", + "None": "Yok", + "OnlyForced": "Sadece Zorunlu" + } + }, + "other": { + "other_title": "Diğer", + "follow_device_orientation": "Otomatik Döndürme", + "video_orientation": "Video Yönü", + "orientation": "Yön", + "orientations": { + "DEFAULT": "Varsayılan", + "ALL": "Tümü", + "PORTRAIT": "Dikey", + "PORTRAIT_UP": "Dikey Yukarı", + "PORTRAIT_DOWN": "Dikey Aşağı", + "LANDSCAPE": "Yatay", + "LANDSCAPE_LEFT": "Yatay Sol", + "LANDSCAPE_RIGHT": "Yatay Sağ", + "OTHER": "Diğer", + "UNKNOWN": "Bilinmeyen" + }, + "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", + "hide_libraries": "Kütüphaneleri Gizle", + "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", + "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" + }, + "downloads": { + "downloads_title": "İndirmeler", + "download_method": "İndirme Yöntemi", + "remux_max_download": "Remux max indirme", + "auto_download": "Otomatik İndirme", + "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", + "save_button": "Kaydet", + "optimized_server": "Optimize Sunucu", + "optimized": "Optimize", + "default": "Varsayılan", + "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Eklentiler", + "jellyseerr": { + "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", + "server_url": "Sunucu URL'si", + "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Şifre", + "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", + "save_button": "Kaydet", + "clear_button": "Temizle", + "login_button": "Giriş Yap", + "total_media_requests": "Toplam medya istekleri", + "movie_quota_limit": "Film kota limiti", + "movie_quota_days": "Film kota günleri", + "tv_quota_limit": "TV kota limiti", + "tv_quota_days": "TV kota günleri", + "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", + "unlimited": "Sınırsız", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Aramasını Etkinleştir ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_marlin": "Marlin hakkında daha fazla oku.", + "save_button": "Kaydet", + "toasts": { + "saved": "Kaydedildi" + } + } + }, + "storage": { + "storage_title": "Depolama", + "app_usage": "Uygulama {{usedSpace}}%", + "device_usage": "Cihaz {{availableSpace}}%", + "size_used": "{{used}} / {{total}} kullanıldı", + "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" + }, + "intro": { + "show_intro": "Tanıtımı Göster", + "reset_intro": "Tanıtımı Sıfırla" + }, + "logs": { + "logs_title": "Günlükler", + "no_logs_available": "Günlükler mevcut değil", + "delete_all_logs": "Tüm günlükleri sil" + }, + "languages": { + "title": "Diller", + "app_language": "Uygulama dili", + "app_language_description": "Uygulama dilini seçin.", + "system": "Sistem" + }, + "toasts": { + "error_deleting_files": "Dosyalar silinirken hata oluştu", + "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", + "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", + "connected": "Bağlandı", + "could_not_connect": "Bağlanılamadı", + "invalid_url": "Geçersiz URL" + } + }, + "downloads": { + "downloads_title": "İndirilenler", + "tvseries": "Diziler", + "movies": "Filmler", + "queue": "Sıra", + "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", + "no_items_in_queue": "Sırada öğe yok", + "no_downloaded_items": "İndirilen öğe yok", + "delete_all_movies_button": "Tüm Filmleri Sil", + "delete_all_tvseries_button": "Tüm Dizileri Sil", + "delete_all_button": "Tümünü Sil", + "active_download": "Aktif indirme", + "no_active_downloads": "Aktif indirme yok", + "active_downloads": "Aktif indirmeler", + "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", + "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", + "back": "Geri", + "delete": "Sil", + "something_went_wrong": "Bir şeyler ters gitti", + "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", + "eta": "Tahmini Süre {{eta}}", + "methods": "Yöntemler", + "toasts": { + "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", + "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", + "failed_to_delete_all_movies": "Filmler silinemedi", + "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", + "failed_to_delete_all_tvseries": "Diziler silinemedi", + "download_cancelled": "İndirme iptal edildi", + "could_not_cancel_download": "İndirme iptal edilemedi", + "download_completed": "İndirme tamamlandı", + "download_started_for": "{{item}} için indirme başlatıldı", + "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", + "download_stated_for_item": "{{item}} için indirme başlatıldı", + "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", + "download_completed_for_item": "{{item}} için indirme tamamlandı", + "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", + "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", + "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", + "no_response_received_from_server": "Sunucudan yanıt alınamadı", + "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", + "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", + "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", + "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", + "go_to_downloads": "İndirmelere git" + } + } + }, + "search": { + "search_here": "Burada ara...", + "search": "Ara...", + "x_items": "{{count}} öge(ler)", + "library": "Kütüphane", + "discover": "Keşfet", + "no_results": "Sonuç bulunamadı", + "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", + "movies": "Filmler", + "series": "Diziler", + "episodes": "Bölümler", + "collections": "Koleksiyonlar", + "actors": "Oyuncular", + "request_movies": "Film Talep Et", + "request_series": "Dizi Talep Et", + "recently_added": "Son Eklenenler", + "recent_requests": "Son Talepler", + "plex_watchlist": "Plex İzleme Listesi", + "trending": "Şu An Popüler", + "popular_movies": "Popüler Filmler", + "movie_genres": "Film Türleri", + "upcoming_movies": "Yaklaşan Filmler", + "studios": "Stüdyolar", + "popular_tv": "Popüler Diziler", + "tv_genres": "Dizi Türleri", + "upcoming_tv": "Yaklaşan Diziler", + "networks": "Ağlar", + "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", + "tmdb_movie_genre": "TMDB Film Türü", + "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", + "tmdb_tv_genre": "TMDB Dizi Türü", + "tmdb_search": "TMDB Arama", + "tmdb_studio": "TMDB Stüdyo", + "tmdb_network": "TMDB Ağ", + "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", + "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" + }, + "library": { + "no_items_found": "Öğe bulunamadı", + "no_results": "Sonuç bulunamadı", + "no_libraries_found": "Kütüphane bulunamadı", + "item_types": { + "movies": "filmler", + "series": "diziler", + "boxsets": "koleksiyonlar", + "items": "ögeler" + }, + "options": { + "display": "Görüntüleme", + "row": "Satır", + "list": "Liste", + "image_style": "Görsel stili", + "poster": "Poster", + "cover": "Kapak", + "show_titles": "Başlıkları göster", + "show_stats": "İstatistikleri göster" + }, + "filters": { + "genres": "Türler", + "years": "Yıllar", + "sort_by": "Sırala", + "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiketler" + } + }, + "favorites": { + "series": "Diziler", + "movies": "Filmler", + "episodes": "Bölümler", + "videos": "Videolar", + "boxsets": "Koleksiyonlar", + "playlists": "Çalma listeleri", + "noDataTitle": "Henüz favori yok", + "noData": "Hızlı erişim için öğeleri favori olarak işaretleyin ve burada görünmelerini sağlayın." + }, + "custom_links": { + "no_links": "Bağlantı yok" + }, + "player": { + "error": "Hata", + "failed_to_get_stream_url": "Yayın URL'si alınamadı", + "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", + "client_error": "İstemci hatası", + "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", + "message_from_server": "Sunucudan mesaj: {{message}}", + "video_has_finished_playing": "Video oynatıldı!", + "no_video_source": "Video kaynağı yok...", + "next_episode": "Sonraki bölüm", + "refresh_tracks": "Parçaları yenile", + "subtitle_tracks": "Altyazı Parçaları:", + "audio_tracks": "Ses Parçaları:", + "playback_state": "Oynatma Durumu:", + "no_data_available": "Veri bulunamadı", + "index": "İndeks:" + }, + "item_card": { + "next_up": "Sıradaki", + "no_items_to_display": "Görüntülenecek öğe yok", + "cast_and_crew": "Oyuncular & Ekip", + "series": "Dizi", + "seasons": "Sezonlar", + "season": "Sezon", + "no_episodes_for_this_season": "Bu sezona ait bölüm yok", + "overview": "Özet", + "more_with": "Daha fazla {{name}}", + "similar_items": "Benzer ögeler", + "no_similar_items_found": "Benzer öge bulunamadı", + "video": "Video", + "more_details": "Daha fazla detay", + "quality": "Kalite", + "audio": "Ses", + "subtitles": "Altyazı", + "show_more": "Daha fazla göster", + "show_less": "Daha az göster", + "appeared_in": "Şurada yer aldı", + "could_not_load_item": "Öge yüklenemedi", + "none": "Hiçbiri", + "download": { + "download_season": "Sezonu indir", + "download_series": "Diziyi indir", + "download_episode": "Bölümü indir", + "download_movie": "Filmi indir", + "download_x_item": "{{item_count}} tane ögeyi indir", + "download_button": "İndir", + "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", + "using_default_method": "Varsayılan yöntem kullanılıyor" + } + }, + "live_tv": { + "next": "Sonraki", + "previous": "Önceki", + "live_tv": "Canlı TV", + "coming_soon": "Yakında", + "on_now": "Şu anda yayında", + "shows": "Programlar", + "movies": "Filmler", + "sports": "Spor", + "for_kids": "Çocuklar İçin", + "news": "Haberler" + }, + "jellyseerr": { + "confirm": "Onayla", + "cancel": "İptal", + "yes": "Evet", + "whats_wrong": "Problem nedir?", + "issue_type": "Sorun türü", + "select_an_issue": "Bir sorun seçin", + "types": "Türler", + "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", + "submit_button": "Gönder", + "report_issue_button": "Sorunu bildir", + "request_button": "Talep et", + "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", + "failed_to_login": "Giriş yapılamadı", + "cast": "Oyuncular", + "details": "Detaylar", + "status": "Durum", + "original_title": "Orijinal Başlık", + "series_type": "Dizi Türü", + "release_dates": "Yayın Tarihleri", + "first_air_date": "İlk Yayın Tarihi", + "next_air_date": "Sonraki Yayın Tarihi", + "revenue": "Gelir", + "budget": "Bütçe", + "original_language": "Orijinal Dil", + "production_country": "Yapım Ülkesi", + "studios": "Stüdyolar", + "network": "Ağ", + "currently_streaming_on": "Şu anda yayınlanıyor", + "advanced": "Gelişmiş", + "request_as": "Şu olarak iste", + "tags": "Etiketler", + "quality_profile": "Kalite Profili", + "root_folder": "Kök Klasör", + "season_all": "Season (all)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "Bölüm {{episode_number}}", + "born": "Doğum", + "appearances": "Görünmeler", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", + "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", + "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", + "issue_submitted": "Sorun gönderildi!", + "requested_item": "{{item}} talep edildi!", + "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", + "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" + } + }, + "tabs": { + "home": "Ana Sayfa", + "search": "Ara", + "library": "Kütüphane", + "custom_links": "Özel Bağlantılar", + "favorites": "Favoriler" + } } diff --git a/translations/zh-CN.json b/translations/zh-CN.json index aad0efd9..7d8e09db 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -1,470 +1,472 @@ { - "login": { - "username_required": "需要用户名", - "error_title": "错误", - "login_title": "登录", - "login_to_title": "登录至", - "username_placeholder": "用户名", - "password_placeholder": "密码", - "login_button": "登录", - "quick_connect": "快速连接", - "enter_code_to_login": "输入代码 {{code}} 以登录", - "failed_to_initiate_quick_connect": "无法启动快速连接", - "got_it": "了解", - "connection_failed": "连接失败", - "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", - "an_unexpected_error_occured": "发生意外错误", - "change_server": "更改服务器", - "invalid_username_or_password": "无效的用户名或密码", - "user_does_not_have_permission_to_log_in": "用户没有登录权限", - "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", - "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", - "there_is_a_server_error": "服务器出错", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "连接", - "previous_servers": "上一个服务器", - "clear_button": "清除", - "search_for_local_servers": "搜索本地服务器", - "searching": "搜索中...", - "servers": "服务器" - }, - "home": { - "no_internet": "无网络", - "no_items": "无项目", - "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", - "go_to_downloads": "前往下载", - "oops": "哎呀!", - "error_message": "出错了。\n请注销重新登录。", - "continue_watching": "继续观看", - "next_up": "下一个", - "recently_added_in": "最近添加于 {{libraryName}}", - "suggested_movies": "推荐电影", - "suggested_episodes": "推荐剧集", - "intro": { - "welcome_to_streamyfin": "欢迎来到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", - "features_title": "功能", - "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", - "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", - "downloads_feature_title": "下载", - "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", - "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", - "centralised_settings_plugin_title": "统一设置插件", - "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", - "done_button": "完成", - "go_to_settings_button": "前往设置", - "read_more": "了解更多" - }, - "settings": { - "settings_title": "设置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用户信息", - "user": "用户", - "server": "服务器", - "token": "密钥", - "app_version": "应用版本" - }, - "quick_connect": { - "quick_connect_title": "快速连接", - "authorize_button": "授权快速连接", - "enter_the_quick_connect_code": "输入快速连接代码...", - "success": "成功", - "quick_connect_autorized": "快速连接已授权", - "error": "错误", - "invalid_code": "无效代码", - "authorize": "授权" - }, - "media_controls": { - "media_controls_title": "媒体控制", - "forward_skip_length": "快进时长", - "rewind_length": "快退时长", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音频", - "set_audio_track": "从上一个项目设置音轨", - "audio_language": "音频语言", - "audio_hint": "选择默认音频语言。", - "none": "无", - "language": "语言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕语言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "从上一个项目设置字幕", - "subtitle_size": "字幕大小", - "subtitle_hint": "设置字幕偏好。", - "none": "无", - "language": "语言", - "loading": "加载中", - "modes": { - "Default": "默认", - "Smart": "智能", - "Always": "总是", - "None": "无", - "OnlyForced": "仅强制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自动旋转", - "video_orientation": "视频方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默认", - "ALL": "全部", - "PORTRAIT": "纵向", - "PORTRAIT_UP": "纵向向上", - "PORTRAIT_DOWN": "纵向向下", - "LANDSCAPE": "横向", - "LANDSCAPE_LEFT": "横向左", - "LANDSCAPE_RIGHT": "横向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全区域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "显示自定义菜单链接", - "hide_libraries": "隐藏媒体库", - "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", - "disable_haptic_feedback": "禁用触觉反馈" - }, - "downloads": { - "downloads_title": "下载", - "download_method": "下载方法", - "remux_max_download": "Remux 最大下载", - "auto_download": "自动下载", - "optimized_versions_server": "Optimized Version 服务器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已优化", - "default": "默认", - "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", - "server_url": "服务器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密码", - "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登录", - "total_media_requests": "总媒体请求", - "movie_quota_limit": "电影配额限制", - "movie_quota_days": "电影配额天数", - "tv_quota_limit": "剧集配额限制", - "tv_quota_days": "剧集配额天数", - "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", - "unlimited": "无限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "启用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_marlin": "查看更多关于 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存储", - "app_usage": "应用 {{usedSpace}}%", - "device_usage": "设备 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "删除所有已下载文件" - }, - "intro": { - "show_intro": "显示介绍", - "reset_intro": "重置介绍" - }, - "logs": { - "logs_title": "日志", - "no_logs_available": "无可用日志", - "delete_all_logs": "删除所有日志" - }, - "languages": { - "title": "语言", - "app_language": "应用语言", - "app_language_description": "选择应用的语言。", - "system": "系统" - }, - "toasts": { - "error_deleting_files": "删除文件时出错", - "background_downloads_enabled": "后台下载已启用", - "background_downloads_disabled": "后台下载已禁用", - "connected": "已连接", - "could_not_connect": "无法连接", - "invalid_url": "无效 URL" - } - }, - "downloads": { - "downloads_title": "下载", - "tvseries": "剧集", - "movies": "电影", - "queue": "队列", - "queue_hint": "应用重启后队列和下载将会丢失", - "no_items_in_queue": "队列中无项目", - "no_downloaded_items": "无已下载项目", - "delete_all_movies_button": "删除所有电影", - "delete_all_tvseries_button": "删除所有剧集", - "delete_all_button": "删除全部", - "active_download": "活跃下载", - "no_active_downloads": "无活跃下载", - "active_downloads": "活跃下载", - "new_app_version_requires_re_download": "更新版本需要重新下载", - "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", - "back": "返回", - "delete": "删除", - "something_went_wrong": "出现问题", - "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", - "eta": "预计完成时间 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您无权下载文件。", - "deleted_all_movies_successfully": "成功删除所有电影!", - "failed_to_delete_all_movies": "删除所有电影失败", - "deleted_all_tvseries_successfully": "成功删除所有剧集!", - "failed_to_delete_all_tvseries": "删除所有剧集失败", - "download_cancelled": "下载已取消", - "could_not_cancel_download": "无法取消下载", - "download_completed": "下载完成", - "download_started_for": "开始下载 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", - "download_stated_for_item": "开始下载 {{item}}", - "download_failed_for_item": "下载失败 {{item}} - {{error}}", - "download_completed_for_item": "下载完成 {{item}}", - "queued_item_for_optimization": "已将 {{item}} 队列进行优化", - "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", - "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", - "no_response_received_from_server": "未收到服务器响应", - "error_setting_up_the_request": "设置请求时出错", - "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", - "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", - "go_to_downloads": "前往下载" - } - } - }, - "search": { - "search_here": "在此搜索...", - "search": "搜索...", - "x_items": "{{count}} 项目", - "library": "媒体库", - "discover": "发现", - "no_results": "没有结果", - "no_results_found_for": "未找到结果", - "movies": "电影", - "series": "剧集", - "episodes": "单集", - "collections": "收藏", - "actors": "演员", - "request_movies": "请求电影", - "request_series": "请求系列", - "recently_added": "最近添加", - "recent_requests": "最近请求", - "plex_watchlist": "Plex 观影清单", - "trending": "趋势", - "popular_movies": "热门电影", - "movie_genres": "电影类型", - "upcoming_movies": "即将上映的电影", - "studios": "工作室", - "popular_tv": "热门电影", - "tv_genres": "剧集类型", - "upcoming_tv": "即将上映的剧集", - "networks": "网络", - "tmdb_movie_keyword": "TMDB 电影关键词", - "tmdb_movie_genre": "TMDB 电影类型", - "tmdb_tv_keyword": "TMDB 剧集关键词", - "tmdb_tv_genre": "TMDB 剧集类型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 网络", - "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", - "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" - }, - "library": { - "no_items_found": "未找到项目", - "no_results": "没有结果", - "no_libraries_found": "未找到媒体库", - "item_types": { - "movies": "电影", - "series": "剧集", - "boxsets": "套装", - "items": "项" - }, - "options": { - "display": "显示", - "row": "行", - "list": "列表", - "image_style": "图片样式", - "poster": "海报", - "cover": "封面", - "show_titles": "显示标题", - "show_stats": "显示统计" - }, - "filters": { - "genres": "类型", - "years": "年份", - "sort_by": "排序依据", - "sort_order": "排序顺序", - "asc": "Ascending", - "desc": "Descending", - "tags": "标签" - } - }, - "favorites": { - "series": "剧集", - "movies": "电影", - "episodes": "单集", - "videos": "视频", - "boxsets": "套装", - "playlists": "播放列表" - }, - "custom_links": { - "no_links": "无链接" - }, - "player": { - "error": "错误", - "failed_to_get_stream_url": "无法获取流 URL", - "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", - "client_error": "客户端错误", - "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", - "message_from_server": "来自服务器的消息", - "video_has_finished_playing": "视频播放完成!", - "no_video_source": "无视频来源...", - "next_episode": "下一集", - "refresh_tracks": "刷新轨道", - "subtitle_tracks": "字幕轨道:", - "audio_tracks": "音频轨道:", - "playback_state": "播放状态:", - "no_data_available": "无可用数据", - "index": "索引:" - }, - "item_card": { - "next_up": "下一个", - "no_items_to_display": "无项目显示", - "cast_and_crew": "演员和工作人员", - "series": "剧集", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季无剧集", - "overview": "概览", - "more_with": "更多 {{name}} 的作品", - "similar_items": "类似项目", - "no_similar_items_found": "未找到类似项目", - "video": "视频", - "more_details": "更多详情", - "quality": "质量", - "audio": "音频", - "subtitles": "字幕", - "show_more": "显示更多", - "show_less": "显示更少", - "appeared_in": "出现于", - "could_not_load_item": "无法加载项目", - "none": "无", - "download": { - "download_season": "下载季", - "download_series": "下载剧集", - "download_episode": "下载单集", - "download_movie": "下载电影", - "download_x_item": "下载 {{item_count}} 项目", - "download_button": "下载", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默认方法" - } - }, - "live_tv": { - "next": "下一个", - "previous": "上一个", - "live_tv": "直播电视", - "coming_soon": "即将播出", - "on_now": "正在播放", - "shows": "节目", - "movies": "电影", - "sports": "体育", - "for_kids": "儿童", - "news": "新闻" - }, - "jellyseerr": { - "confirm": "确认", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什么问题?", - "issue_type": "问题类型", - "select_an_issue": "选择一个问题", - "types": "类型", - "describe_the_issue": "(可选)描述问题...", - "submit_button": "提交", - "report_issue_button": "报告问题", - "request_button": "请求", - "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", - "failed_to_login": "登录失败", - "cast": "演员", - "details": "详情", - "status": "状态", - "original_title": "原标题", - "series_type": "剧集类型", - "release_dates": "发行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "预算", - "original_language": "原始语言", - "production_country": "制作国家/地区", - "studios": "工作室", - "network": "网络", - "currently_streaming_on": "目前在以下流媒体上播放", - "advanced": "高级设置", - "request_as": "选择用户以请求", - "tags": "标签", - "quality_profile": "质量配置文件", - "root_folder": "根文件夹", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出场", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", - "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", - "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", - "issue_submitted": "问题已提交!", - "requested_item": "已请求 {{item}}!", - "you_dont_have_permission_to_request": "您无权请求媒体!", - "something_went_wrong_requesting_media": "请求媒体时出了些问题!" - } - }, - "tabs": { - "home": "主页", - "search": "搜索", - "library": "媒体库", - "custom_links": "自定义链接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用户名", + "error_title": "错误", + "login_title": "登录", + "login_to_title": "登录至", + "username_placeholder": "用户名", + "password_placeholder": "密码", + "login_button": "登录", + "quick_connect": "快速连接", + "enter_code_to_login": "输入代码 {{code}} 以登录", + "failed_to_initiate_quick_connect": "无法启动快速连接", + "got_it": "了解", + "connection_failed": "连接失败", + "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", + "an_unexpected_error_occured": "发生意外错误", + "change_server": "更改服务器", + "invalid_username_or_password": "无效的用户名或密码", + "user_does_not_have_permission_to_log_in": "用户没有登录权限", + "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", + "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", + "there_is_a_server_error": "服务器出错", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "连接", + "previous_servers": "上一个服务器", + "clear_button": "清除", + "search_for_local_servers": "搜索本地服务器", + "searching": "搜索中...", + "servers": "服务器" + }, + "home": { + "no_internet": "无网络", + "no_items": "无项目", + "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", + "go_to_downloads": "前往下载", + "oops": "哎呀!", + "error_message": "出错了。\n请注销重新登录。", + "continue_watching": "继续观看", + "next_up": "下一个", + "recently_added_in": "最近添加于 {{libraryName}}", + "suggested_movies": "推荐电影", + "suggested_episodes": "推荐剧集", + "intro": { + "welcome_to_streamyfin": "欢迎来到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", + "features_title": "功能", + "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", + "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", + "downloads_feature_title": "下载", + "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", + "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", + "centralised_settings_plugin_title": "统一设置插件", + "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", + "done_button": "完成", + "go_to_settings_button": "前往设置", + "read_more": "了解更多" + }, + "settings": { + "settings_title": "设置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用户信息", + "user": "用户", + "server": "服务器", + "token": "密钥", + "app_version": "应用版本" + }, + "quick_connect": { + "quick_connect_title": "快速连接", + "authorize_button": "授权快速连接", + "enter_the_quick_connect_code": "输入快速连接代码...", + "success": "成功", + "quick_connect_autorized": "快速连接已授权", + "error": "错误", + "invalid_code": "无效代码", + "authorize": "授权" + }, + "media_controls": { + "media_controls_title": "媒体控制", + "forward_skip_length": "快进时长", + "rewind_length": "快退时长", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音频", + "set_audio_track": "从上一个项目设置音轨", + "audio_language": "音频语言", + "audio_hint": "选择默认音频语言。", + "none": "无", + "language": "语言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕语言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "从上一个项目设置字幕", + "subtitle_size": "字幕大小", + "subtitle_hint": "设置字幕偏好。", + "none": "无", + "language": "语言", + "loading": "加载中", + "modes": { + "Default": "默认", + "Smart": "智能", + "Always": "总是", + "None": "无", + "OnlyForced": "仅强制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自动旋转", + "video_orientation": "视频方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默认", + "ALL": "全部", + "PORTRAIT": "纵向", + "PORTRAIT_UP": "纵向向上", + "PORTRAIT_DOWN": "纵向向下", + "LANDSCAPE": "横向", + "LANDSCAPE_LEFT": "横向左", + "LANDSCAPE_RIGHT": "横向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全区域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "显示自定义菜单链接", + "hide_libraries": "隐藏媒体库", + "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", + "disable_haptic_feedback": "禁用触觉反馈" + }, + "downloads": { + "downloads_title": "下载", + "download_method": "下载方法", + "remux_max_download": "Remux 最大下载", + "auto_download": "自动下载", + "optimized_versions_server": "Optimized Version 服务器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已优化", + "default": "默认", + "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", + "server_url": "服务器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密码", + "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登录", + "total_media_requests": "总媒体请求", + "movie_quota_limit": "电影配额限制", + "movie_quota_days": "电影配额天数", + "tv_quota_limit": "剧集配额限制", + "tv_quota_days": "剧集配额天数", + "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", + "unlimited": "无限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "启用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_marlin": "查看更多关于 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存储", + "app_usage": "应用 {{usedSpace}}%", + "device_usage": "设备 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "删除所有已下载文件" + }, + "intro": { + "show_intro": "显示介绍", + "reset_intro": "重置介绍" + }, + "logs": { + "logs_title": "日志", + "no_logs_available": "无可用日志", + "delete_all_logs": "删除所有日志" + }, + "languages": { + "title": "语言", + "app_language": "应用语言", + "app_language_description": "选择应用的语言。", + "system": "系统" + }, + "toasts": { + "error_deleting_files": "删除文件时出错", + "background_downloads_enabled": "后台下载已启用", + "background_downloads_disabled": "后台下载已禁用", + "connected": "已连接", + "could_not_connect": "无法连接", + "invalid_url": "无效 URL" + } + }, + "downloads": { + "downloads_title": "下载", + "tvseries": "剧集", + "movies": "电影", + "queue": "队列", + "queue_hint": "应用重启后队列和下载将会丢失", + "no_items_in_queue": "队列中无项目", + "no_downloaded_items": "无已下载项目", + "delete_all_movies_button": "删除所有电影", + "delete_all_tvseries_button": "删除所有剧集", + "delete_all_button": "删除全部", + "active_download": "活跃下载", + "no_active_downloads": "无活跃下载", + "active_downloads": "活跃下载", + "new_app_version_requires_re_download": "更新版本需要重新下载", + "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", + "back": "返回", + "delete": "删除", + "something_went_wrong": "出现问题", + "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", + "eta": "预计完成时间 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您无权下载文件。", + "deleted_all_movies_successfully": "成功删除所有电影!", + "failed_to_delete_all_movies": "删除所有电影失败", + "deleted_all_tvseries_successfully": "成功删除所有剧集!", + "failed_to_delete_all_tvseries": "删除所有剧集失败", + "download_cancelled": "下载已取消", + "could_not_cancel_download": "无法取消下载", + "download_completed": "下载完成", + "download_started_for": "开始下载 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", + "download_stated_for_item": "开始下载 {{item}}", + "download_failed_for_item": "下载失败 {{item}} - {{error}}", + "download_completed_for_item": "下载完成 {{item}}", + "queued_item_for_optimization": "已将 {{item}} 队列进行优化", + "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", + "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", + "no_response_received_from_server": "未收到服务器响应", + "error_setting_up_the_request": "设置请求时出错", + "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", + "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", + "go_to_downloads": "前往下载" + } + } + }, + "search": { + "search_here": "在此搜索...", + "search": "搜索...", + "x_items": "{{count}} 项目", + "library": "媒体库", + "discover": "发现", + "no_results": "没有结果", + "no_results_found_for": "未找到结果", + "movies": "电影", + "series": "剧集", + "episodes": "单集", + "collections": "收藏", + "actors": "演员", + "request_movies": "请求电影", + "request_series": "请求系列", + "recently_added": "最近添加", + "recent_requests": "最近请求", + "plex_watchlist": "Plex 观影清单", + "trending": "趋势", + "popular_movies": "热门电影", + "movie_genres": "电影类型", + "upcoming_movies": "即将上映的电影", + "studios": "工作室", + "popular_tv": "热门电影", + "tv_genres": "剧集类型", + "upcoming_tv": "即将上映的剧集", + "networks": "网络", + "tmdb_movie_keyword": "TMDB 电影关键词", + "tmdb_movie_genre": "TMDB 电影类型", + "tmdb_tv_keyword": "TMDB 剧集关键词", + "tmdb_tv_genre": "TMDB 剧集类型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 网络", + "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", + "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" + }, + "library": { + "no_items_found": "未找到项目", + "no_results": "没有结果", + "no_libraries_found": "未找到媒体库", + "item_types": { + "movies": "电影", + "series": "剧集", + "boxsets": "套装", + "items": "项" + }, + "options": { + "display": "显示", + "row": "行", + "list": "列表", + "image_style": "图片样式", + "poster": "海报", + "cover": "封面", + "show_titles": "显示标题", + "show_stats": "显示统计" + }, + "filters": { + "genres": "类型", + "years": "年份", + "sort_by": "排序依据", + "sort_order": "排序顺序", + "asc": "Ascending", + "desc": "Descending", + "tags": "标签" + } + }, + "favorites": { + "series": "剧集", + "movies": "电影", + "episodes": "单集", + "videos": "视频", + "boxsets": "套装", + "playlists": "播放列表", + "noDataTitle": "暂无收藏", + "noData": "将项目标记为收藏,它们将显示在此处以便快速访问。" + }, + "custom_links": { + "no_links": "无链接" + }, + "player": { + "error": "错误", + "failed_to_get_stream_url": "无法获取流 URL", + "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", + "client_error": "客户端错误", + "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", + "message_from_server": "来自服务器的消息", + "video_has_finished_playing": "视频播放完成!", + "no_video_source": "无视频来源...", + "next_episode": "下一集", + "refresh_tracks": "刷新轨道", + "subtitle_tracks": "字幕轨道:", + "audio_tracks": "音频轨道:", + "playback_state": "播放状态:", + "no_data_available": "无可用数据", + "index": "索引:" + }, + "item_card": { + "next_up": "下一个", + "no_items_to_display": "无项目显示", + "cast_and_crew": "演员和工作人员", + "series": "剧集", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季无剧集", + "overview": "概览", + "more_with": "更多 {{name}} 的作品", + "similar_items": "类似项目", + "no_similar_items_found": "未找到类似项目", + "video": "视频", + "more_details": "更多详情", + "quality": "质量", + "audio": "音频", + "subtitles": "字幕", + "show_more": "显示更多", + "show_less": "显示更少", + "appeared_in": "出现于", + "could_not_load_item": "无法加载项目", + "none": "无", + "download": { + "download_season": "下载季", + "download_series": "下载剧集", + "download_episode": "下载单集", + "download_movie": "下载电影", + "download_x_item": "下载 {{item_count}} 项目", + "download_button": "下载", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默认方法" + } + }, + "live_tv": { + "next": "下一个", + "previous": "上一个", + "live_tv": "直播电视", + "coming_soon": "即将播出", + "on_now": "正在播放", + "shows": "节目", + "movies": "电影", + "sports": "体育", + "for_kids": "儿童", + "news": "新闻" + }, + "jellyseerr": { + "confirm": "确认", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什么问题?", + "issue_type": "问题类型", + "select_an_issue": "选择一个问题", + "types": "类型", + "describe_the_issue": "(可选)描述问题...", + "submit_button": "提交", + "report_issue_button": "报告问题", + "request_button": "请求", + "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", + "failed_to_login": "登录失败", + "cast": "演员", + "details": "详情", + "status": "状态", + "original_title": "原标题", + "series_type": "剧集类型", + "release_dates": "发行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "预算", + "original_language": "原始语言", + "production_country": "制作国家/地区", + "studios": "工作室", + "network": "网络", + "currently_streaming_on": "目前在以下流媒体上播放", + "advanced": "高级设置", + "request_as": "选择用户以请求", + "tags": "标签", + "quality_profile": "质量配置文件", + "root_folder": "根文件夹", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出场", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", + "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", + "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", + "issue_submitted": "问题已提交!", + "requested_item": "已请求 {{item}}!", + "you_dont_have_permission_to_request": "您无权请求媒体!", + "something_went_wrong_requesting_media": "请求媒体时出了些问题!" + } + }, + "tabs": { + "home": "主页", + "search": "搜索", + "library": "媒体库", + "custom_links": "自定义链接", + "favorites": "收藏" + } } diff --git a/translations/zh-TW.json b/translations/zh-TW.json index cfe2f7ce..5cfb2066 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -1,470 +1,472 @@ { - "login": { - "username_required": "需要用戶名", - "error_title": "錯誤", - "login_title": "登入", - "login_to_title": "登入至", - "username_placeholder": "用戶名", - "password_placeholder": "密碼", - "login_button": "登入", - "quick_connect": "快速連接", - "enter_code_to_login": "輸入代碼 {{code}} 以登入", - "failed_to_initiate_quick_connect": "無法啟動快速連接", - "got_it": "知道了", - "connection_failed": "連接失敗", - "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", - "an_unexpected_error_occured": "發生意外錯誤", - "change_server": "更改伺服器", - "invalid_username_or_password": "無效的用戶名或密碼", - "user_does_not_have_permission_to_log_in": "用戶無權登入", - "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", - "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", - "there_is_a_server_error": "伺服器出錯", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "連接", - "previous_servers": "先前的伺服器", - "clear_button": "清除", - "search_for_local_servers": "搜尋本地伺服器", - "searching": "搜尋中...", - "servers": "伺服器" - }, - "home": { - "no_internet": "無網絡", - "no_items": "無項目", - "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", - "go_to_downloads": "前往下載", - "oops": "哎呀!", - "error_message": "出錯了。\n請重新登出並登入。", - "continue_watching": "繼續觀看", - "next_up": "下一個", - "recently_added_in": "最近添加於 {{libraryName}}", - "suggested_movies": "推薦電影", - "suggested_episodes": "推薦劇集", - "intro": { - "welcome_to_streamyfin": "歡迎來到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", - "features_title": "功能", - "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", - "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", - "downloads_feature_title": "下載", - "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", - "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", - "centralised_settings_plugin_title": "統一設置插件", - "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", - "done_button": "完成", - "go_to_settings_button": "前往設置", - "read_more": "閱讀更多" - }, - "settings": { - "settings_title": "設置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用戶信息", - "user": "用戶", - "server": "伺服器", - "token": "令牌", - "app_version": "應用版本" - }, - "quick_connect": { - "quick_connect_title": "快速連接", - "authorize_button": "授權快速連接", - "enter_the_quick_connect_code": "輸入快速連接代碼...", - "success": "成功", - "quick_connect_autorized": "快速連接已授權", - "error": "錯誤", - "invalid_code": "無效代碼", - "authorize": "授權" - }, - "media_controls": { - "media_controls_title": "媒體控制", - "forward_skip_length": "快進秒數", - "rewind_length": "倒帶秒數", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音頻", - "set_audio_track": "從上一個項目設置音軌", - "audio_language": "音頻語言", - "audio_hint": "選擇默認音頻語言。", - "none": "無", - "language": "語言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕語言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "從上一個項目設置字幕軌道", - "subtitle_size": "字幕大小", - "subtitle_hint": "配置字幕偏好。", - "none": "無", - "language": "語言", - "loading": "加載中", - "modes": { - "Default": "默認", - "Smart": "智能", - "Always": "總是", - "None": "無", - "OnlyForced": "僅強制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自動旋轉", - "video_orientation": "影片方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默認", - "ALL": "全部", - "PORTRAIT": "縱向", - "PORTRAIT_UP": "縱向向上", - "PORTRAIT_DOWN": "縱向向下", - "LANDSCAPE": "橫向", - "LANDSCAPE_LEFT": "橫向左", - "LANDSCAPE_RIGHT": "橫向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全區域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "顯示自定義菜單鏈接", - "hide_libraries": "隱藏媒體庫", - "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", - "disable_haptic_feedback": "禁用觸覺回饋" - }, - "downloads": { - "downloads_title": "下載", - "download_method": "下載方法", - "remux_max_download": "Remux 最大下載", - "auto_download": "自動下載", - "optimized_versions_server": "Optimized Version 伺服器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已優化", - "default": "默認", - "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", - "server_url": "伺服器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密碼", - "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登入", - "total_media_requests": "總媒體請求", - "movie_quota_limit": "電影配額限制", - "movie_quota_days": "電影配額天數", - "tv_quota_limit": "電視配額限制", - "tv_quota_days": "電視配額天數", - "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", - "unlimited": "無限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "啟用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存儲", - "app_usage": "應用 {{usedSpace}}%", - "device_usage": "設備 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "刪除所有已下載文件" - }, - "intro": { - "show_intro": "顯示介紹", - "reset_intro": "重置介紹" - }, - "logs": { - "logs_title": "日誌", - "no_logs_available": "無可用日誌", - "delete_all_logs": "刪除所有日誌" - }, - "languages": { - "title": "語言", - "app_language": "應用語言", - "app_language_description": "選擇應用的語言。", - "system": "系統" - }, - "toasts": { - "error_deleting_files": "刪除文件時出錯", - "background_downloads_enabled": "背景下載已啟用", - "background_downloads_disabled": "背景下載已禁用", - "connected": "已連接", - "could_not_connect": "無法連接", - "invalid_url": "無效的 URL" - } - }, - "downloads": { - "downloads_title": "下載", - "tvseries": "電視劇", - "movies": "電影", - "queue": "隊列", - "queue_hint": "應用重啟後隊列和下載將會丟失", - "no_items_in_queue": "隊列中無項目", - "no_downloaded_items": "無已下載項目", - "delete_all_movies_button": "刪除所有電影", - "delete_all_tvseries_button": "刪除所有電視劇", - "delete_all_button": "刪除全部", - "active_download": "活動下載", - "no_active_downloads": "無活動下載", - "active_downloads": "活動下載", - "new_app_version_requires_re_download": "新應用版本需要重新下載", - "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", - "back": "返回", - "delete": "刪除", - "something_went_wrong": "出了些問題", - "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", - "eta": "預計完成時間 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您無權下載文件。", - "deleted_all_movies_successfully": "成功刪除所有電影!", - "failed_to_delete_all_movies": "刪除所有電影失敗", - "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", - "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", - "download_cancelled": "下載已取消", - "could_not_cancel_download": "無法取消下載", - "download_completed": "下載完成", - "download_started_for": "開始下載 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", - "download_stated_for_item": "開始下載 {{item}}", - "download_failed_for_item": "下載失敗 {{item}} - {{error}}", - "download_completed_for_item": "下載完成 {{item}}", - "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", - "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", - "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", - "no_response_received_from_server": "未收到伺服器的響應", - "error_setting_up_the_request": "設置請求時出錯", - "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", - "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", - "go_to_downloads": "前往下載" - } - } - }, - "search": { - "search_here": "在這裡搜索...", - "search": "搜索...", - "x_items": "{{count}} 項目", - "library": "媒體庫", - "discover": "發現", - "no_results": "沒有結果", - "no_results_found_for": "未找到結果", - "movies": "電影", - "series": "系列", - "episodes": "劇集", - "collections": "收藏", - "actors": "演員", - "request_movies": "請求電影", - "request_series": "請求系列", - "recently_added": "最近添加", - "recent_requests": "最近請求", - "plex_watchlist": "Plex 觀影清單", - "trending": "趨勢", - "popular_movies": "熱門電影", - "movie_genres": "電影類型", - "upcoming_movies": "即將上映的電影", - "studios": "工作室", - "popular_tv": "熱門電視", - "tv_genres": "電視類型", - "upcoming_tv": "即將上映的電視", - "networks": "網絡", - "tmdb_movie_keyword": "TMDB 電影關鍵詞", - "tmdb_movie_genre": "TMDB 電影類型", - "tmdb_tv_keyword": "TMDB 電視關鍵詞", - "tmdb_tv_genre": "TMDB 電視類型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 網絡", - "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", - "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" - }, - "library": { - "no_items_found": "未找到項目", - "no_results": "沒有結果", - "no_libraries_found": "未找到媒體庫", - "item_types": { - "movies": "電影", - "series": "系列", - "boxsets": "套裝", - "items": "項目" - }, - "options": { - "display": "顯示", - "row": "行", - "list": "列表", - "image_style": "圖片樣式", - "poster": "海報", - "cover": "封面", - "show_titles": "顯示標題", - "show_stats": "顯示統計" - }, - "filters": { - "genres": "類型", - "years": "年份", - "sort_by": "排序依據", - "sort_order": "排序順序", - "asc": "Ascending", - "desc": "Descending", - "tags": "標籤" - } - }, - "favorites": { - "series": "系列", - "movies": "電影", - "episodes": "劇集", - "videos": "影片", - "boxsets": "套裝", - "playlists": "播放列表" - }, - "custom_links": { - "no_links": "無鏈接" - }, - "player": { - "error": "錯誤", - "failed_to_get_stream_url": "無法獲取流 URL", - "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", - "client_error": "客戶端錯誤", - "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", - "message_from_server": "來自伺服器的消息", - "video_has_finished_playing": "影片播放完畢!", - "no_video_source": "無影片來源...", - "next_episode": "下一集", - "refresh_tracks": "刷新軌道", - "subtitle_tracks": "字幕軌道:", - "audio_tracks": "音頻軌道:", - "playback_state": "播放狀態:", - "no_data_available": "無可用數據", - "index": "索引:" - }, - "item_card": { - "next_up": "下一個", - "no_items_to_display": "無項目顯示", - "cast_and_crew": "演員和工作人員", - "series": "系列", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季無劇集", - "overview": "概覽", - "more_with": "更多 {{name}} 的作品", - "similar_items": "類似項目", - "no_similar_items_found": "未找到類似項目", - "video": "影片", - "more_details": "更多詳情", - "quality": "質量", - "audio": "音頻", - "subtitles": "字幕", - "show_more": "顯示更多", - "show_less": "顯示更少", - "appeared_in": "出現於", - "could_not_load_item": "無法加載項目", - "none": "無", - "download": { - "download_season": "下載季度", - "download_series": "下載系列", - "download_episode": "下載劇集", - "download_movie": "下載電影", - "download_x_item": "下載 {{item_count}} 項目", - "download_button": "下載", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默認方法" - } - }, - "live_tv": { - "next": "下一個", - "previous": "上一個", - "live_tv": "直播電視", - "coming_soon": "即將推出", - "on_now": "正在播放", - "shows": "節目", - "movies": "電影", - "sports": "體育", - "for_kids": "兒童", - "news": "新聞" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什麼問題?", - "issue_type": "問題類型", - "select_an_issue": "選擇一個問題", - "types": "類型", - "describe_the_issue": "(可選)描述問題...", - "submit_button": "提交", - "report_issue_button": "報告問題", - "request_button": "請求", - "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", - "failed_to_login": "登入失敗", - "cast": "演員", - "details": "詳情", - "status": "狀態", - "original_title": "原標題", - "series_type": "系列類型", - "release_dates": "發行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "預算", - "original_language": "原始語言", - "production_country": "製作國家", - "studios": "工作室", - "network": "網絡", - "currently_streaming_on": "目前在以下流媒體上播放", - "advanced": "高級設定", - "request_as": "選擇用戶以作請求", - "tags": "標籤", - "quality_profile": "質量配置文件", - "root_folder": "根文件夾", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出場", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", - "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", - "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", - "issue_submitted": "問題已提交!", - "requested_item": "已請求 {{item}}!", - "you_dont_have_permission_to_request": "您無權請求媒體!", - "something_went_wrong_requesting_media": "請求媒體時出了些問題!" - } - }, - "tabs": { - "home": "主頁", - "search": "搜索", - "library": "媒體庫", - "custom_links": "自定義鏈接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用戶名", + "error_title": "錯誤", + "login_title": "登入", + "login_to_title": "登入至", + "username_placeholder": "用戶名", + "password_placeholder": "密碼", + "login_button": "登入", + "quick_connect": "快速連接", + "enter_code_to_login": "輸入代碼 {{code}} 以登入", + "failed_to_initiate_quick_connect": "無法啟動快速連接", + "got_it": "知道了", + "connection_failed": "連接失敗", + "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", + "an_unexpected_error_occured": "發生意外錯誤", + "change_server": "更改伺服器", + "invalid_username_or_password": "無效的用戶名或密碼", + "user_does_not_have_permission_to_log_in": "用戶無權登入", + "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", + "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", + "there_is_a_server_error": "伺服器出錯", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "連接", + "previous_servers": "先前的伺服器", + "clear_button": "清除", + "search_for_local_servers": "搜尋本地伺服器", + "searching": "搜尋中...", + "servers": "伺服器" + }, + "home": { + "no_internet": "無網絡", + "no_items": "無項目", + "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", + "go_to_downloads": "前往下載", + "oops": "哎呀!", + "error_message": "出錯了。\n請重新登出並登入。", + "continue_watching": "繼續觀看", + "next_up": "下一個", + "recently_added_in": "最近添加於 {{libraryName}}", + "suggested_movies": "推薦電影", + "suggested_episodes": "推薦劇集", + "intro": { + "welcome_to_streamyfin": "歡迎來到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", + "features_title": "功能", + "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", + "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", + "downloads_feature_title": "下載", + "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", + "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", + "centralised_settings_plugin_title": "統一設置插件", + "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", + "done_button": "完成", + "go_to_settings_button": "前往設置", + "read_more": "閱讀更多" + }, + "settings": { + "settings_title": "設置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用戶信息", + "user": "用戶", + "server": "伺服器", + "token": "令牌", + "app_version": "應用版本" + }, + "quick_connect": { + "quick_connect_title": "快速連接", + "authorize_button": "授權快速連接", + "enter_the_quick_connect_code": "輸入快速連接代碼...", + "success": "成功", + "quick_connect_autorized": "快速連接已授權", + "error": "錯誤", + "invalid_code": "無效代碼", + "authorize": "授權" + }, + "media_controls": { + "media_controls_title": "媒體控制", + "forward_skip_length": "快進秒數", + "rewind_length": "倒帶秒數", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音頻", + "set_audio_track": "從上一個項目設置音軌", + "audio_language": "音頻語言", + "audio_hint": "選擇默認音頻語言。", + "none": "無", + "language": "語言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕語言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "從上一個項目設置字幕軌道", + "subtitle_size": "字幕大小", + "subtitle_hint": "配置字幕偏好。", + "none": "無", + "language": "語言", + "loading": "加載中", + "modes": { + "Default": "默認", + "Smart": "智能", + "Always": "總是", + "None": "無", + "OnlyForced": "僅強制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自動旋轉", + "video_orientation": "影片方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默認", + "ALL": "全部", + "PORTRAIT": "縱向", + "PORTRAIT_UP": "縱向向上", + "PORTRAIT_DOWN": "縱向向下", + "LANDSCAPE": "橫向", + "LANDSCAPE_LEFT": "橫向左", + "LANDSCAPE_RIGHT": "橫向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全區域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "顯示自定義菜單鏈接", + "hide_libraries": "隱藏媒體庫", + "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", + "disable_haptic_feedback": "禁用觸覺回饋" + }, + "downloads": { + "downloads_title": "下載", + "download_method": "下載方法", + "remux_max_download": "Remux 最大下載", + "auto_download": "自動下載", + "optimized_versions_server": "Optimized Version 伺服器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已優化", + "default": "默認", + "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", + "server_url": "伺服器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密碼", + "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登入", + "total_media_requests": "總媒體請求", + "movie_quota_limit": "電影配額限制", + "movie_quota_days": "電影配額天數", + "tv_quota_limit": "電視配額限制", + "tv_quota_days": "電視配額天數", + "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", + "unlimited": "無限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "啟用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存儲", + "app_usage": "應用 {{usedSpace}}%", + "device_usage": "設備 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "刪除所有已下載文件" + }, + "intro": { + "show_intro": "顯示介紹", + "reset_intro": "重置介紹" + }, + "logs": { + "logs_title": "日誌", + "no_logs_available": "無可用日誌", + "delete_all_logs": "刪除所有日誌" + }, + "languages": { + "title": "語言", + "app_language": "應用語言", + "app_language_description": "選擇應用的語言。", + "system": "系統" + }, + "toasts": { + "error_deleting_files": "刪除文件時出錯", + "background_downloads_enabled": "背景下載已啟用", + "background_downloads_disabled": "背景下載已禁用", + "connected": "已連接", + "could_not_connect": "無法連接", + "invalid_url": "無效的 URL" + } + }, + "downloads": { + "downloads_title": "下載", + "tvseries": "電視劇", + "movies": "電影", + "queue": "隊列", + "queue_hint": "應用重啟後隊列和下載將會丟失", + "no_items_in_queue": "隊列中無項目", + "no_downloaded_items": "無已下載項目", + "delete_all_movies_button": "刪除所有電影", + "delete_all_tvseries_button": "刪除所有電視劇", + "delete_all_button": "刪除全部", + "active_download": "活動下載", + "no_active_downloads": "無活動下載", + "active_downloads": "活動下載", + "new_app_version_requires_re_download": "新應用版本需要重新下載", + "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", + "back": "返回", + "delete": "刪除", + "something_went_wrong": "出了些問題", + "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", + "eta": "預計完成時間 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您無權下載文件。", + "deleted_all_movies_successfully": "成功刪除所有電影!", + "failed_to_delete_all_movies": "刪除所有電影失敗", + "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", + "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", + "download_cancelled": "下載已取消", + "could_not_cancel_download": "無法取消下載", + "download_completed": "下載完成", + "download_started_for": "開始下載 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", + "download_stated_for_item": "開始下載 {{item}}", + "download_failed_for_item": "下載失敗 {{item}} - {{error}}", + "download_completed_for_item": "下載完成 {{item}}", + "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", + "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", + "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", + "no_response_received_from_server": "未收到伺服器的響應", + "error_setting_up_the_request": "設置請求時出錯", + "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", + "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", + "go_to_downloads": "前往下載" + } + } + }, + "search": { + "search_here": "在這裡搜索...", + "search": "搜索...", + "x_items": "{{count}} 項目", + "library": "媒體庫", + "discover": "發現", + "no_results": "沒有結果", + "no_results_found_for": "未找到結果", + "movies": "電影", + "series": "系列", + "episodes": "劇集", + "collections": "收藏", + "actors": "演員", + "request_movies": "請求電影", + "request_series": "請求系列", + "recently_added": "最近添加", + "recent_requests": "最近請求", + "plex_watchlist": "Plex 觀影清單", + "trending": "趨勢", + "popular_movies": "熱門電影", + "movie_genres": "電影類型", + "upcoming_movies": "即將上映的電影", + "studios": "工作室", + "popular_tv": "熱門電視", + "tv_genres": "電視類型", + "upcoming_tv": "即將上映的電視", + "networks": "網絡", + "tmdb_movie_keyword": "TMDB 電影關鍵詞", + "tmdb_movie_genre": "TMDB 電影類型", + "tmdb_tv_keyword": "TMDB 電視關鍵詞", + "tmdb_tv_genre": "TMDB 電視類型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 網絡", + "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", + "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" + }, + "library": { + "no_items_found": "未找到項目", + "no_results": "沒有結果", + "no_libraries_found": "未找到媒體庫", + "item_types": { + "movies": "電影", + "series": "系列", + "boxsets": "套裝", + "items": "項目" + }, + "options": { + "display": "顯示", + "row": "行", + "list": "列表", + "image_style": "圖片樣式", + "poster": "海報", + "cover": "封面", + "show_titles": "顯示標題", + "show_stats": "顯示統計" + }, + "filters": { + "genres": "類型", + "years": "年份", + "sort_by": "排序依據", + "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", + "tags": "標籤" + } + }, + "favorites": { + "series": "系列", + "movies": "電影", + "episodes": "劇集", + "videos": "影片", + "boxsets": "套裝", + "playlists": "播放列表", + "noDataTitle": "尚無收藏", + "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" + }, + "custom_links": { + "no_links": "無鏈接" + }, + "player": { + "error": "錯誤", + "failed_to_get_stream_url": "無法獲取流 URL", + "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", + "client_error": "客戶端錯誤", + "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", + "message_from_server": "來自伺服器的消息", + "video_has_finished_playing": "影片播放完畢!", + "no_video_source": "無影片來源...", + "next_episode": "下一集", + "refresh_tracks": "刷新軌道", + "subtitle_tracks": "字幕軌道:", + "audio_tracks": "音頻軌道:", + "playback_state": "播放狀態:", + "no_data_available": "無可用數據", + "index": "索引:" + }, + "item_card": { + "next_up": "下一個", + "no_items_to_display": "無項目顯示", + "cast_and_crew": "演員和工作人員", + "series": "系列", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季無劇集", + "overview": "概覽", + "more_with": "更多 {{name}} 的作品", + "similar_items": "類似項目", + "no_similar_items_found": "未找到類似項目", + "video": "影片", + "more_details": "更多詳情", + "quality": "質量", + "audio": "音頻", + "subtitles": "字幕", + "show_more": "顯示更多", + "show_less": "顯示更少", + "appeared_in": "出現於", + "could_not_load_item": "無法加載項目", + "none": "無", + "download": { + "download_season": "下載季度", + "download_series": "下載系列", + "download_episode": "下載劇集", + "download_movie": "下載電影", + "download_x_item": "下載 {{item_count}} 項目", + "download_button": "下載", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默認方法" + } + }, + "live_tv": { + "next": "下一個", + "previous": "上一個", + "live_tv": "直播電視", + "coming_soon": "即將推出", + "on_now": "正在播放", + "shows": "節目", + "movies": "電影", + "sports": "體育", + "for_kids": "兒童", + "news": "新聞" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什麼問題?", + "issue_type": "問題類型", + "select_an_issue": "選擇一個問題", + "types": "類型", + "describe_the_issue": "(可選)描述問題...", + "submit_button": "提交", + "report_issue_button": "報告問題", + "request_button": "請求", + "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", + "failed_to_login": "登入失敗", + "cast": "演員", + "details": "詳情", + "status": "狀態", + "original_title": "原標題", + "series_type": "系列類型", + "release_dates": "發行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "預算", + "original_language": "原始語言", + "production_country": "製作國家", + "studios": "工作室", + "network": "網絡", + "currently_streaming_on": "目前在以下流媒體上播放", + "advanced": "高級設定", + "request_as": "選擇用戶以作請求", + "tags": "標籤", + "quality_profile": "質量配置文件", + "root_folder": "根文件夾", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出場", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", + "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", + "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", + "issue_submitted": "問題已提交!", + "requested_item": "已請求 {{item}}!", + "you_dont_have_permission_to_request": "您無權請求媒體!", + "something_went_wrong_requesting_media": "請求媒體時出了些問題!" + } + }, + "tabs": { + "home": "主頁", + "search": "搜索", + "library": "媒體庫", + "custom_links": "自定義鏈接", + "favorites": "收藏" + } } diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 00000000..8be427bb --- /dev/null +++ b/types.d.ts @@ -0,0 +1,9 @@ +declare module "*.svg" { + const content: any; + export default content; +} + +declare module "*.png" { + const value: any; + export default value; +} From a1b2248f165642fe27021a39eb217b912b557bd7 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sun, 16 Mar 2025 08:12:22 +0100 Subject: [PATCH 79/93] chore: add biome configuration (#590) --- .vscode/settings.json | 36 ++++++++++++++++++++++-------------- biome.json | 42 ++++++++++++++++++++++++++++++++++++++++++ bun.lock | 19 +++++++++++++++++++ package.json | 11 ++++++----- 4 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 biome.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 42b83625..c7cdc61f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,24 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "prettier.printWidth": 120, - "[swift]": { - "editor.defaultFormatter": "sswg.swift-lang" - } + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "prettier.printWidth": 120, + "[swift]": { + "editor.defaultFormatter": "sswg.swift-lang" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + } } diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..f429e9bc --- /dev/null +++ b/biome.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", + "organizeImports": { + "enabled": true + }, + "files": {}, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { "useExhaustiveDependencies": "off" }, + "suspicious": { + "noExplicitAny": "off" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "attributePosition": "auto", + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + }, + "javascript": { + "formatter": { + "arrowParentheses": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "jsxQuoteStyle": "single", + "quoteProperties": "asNeeded", + "semicolons": "always", + "lineWidth": 80 + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + } +} diff --git a/bun.lock b/bun.lock index b3c75406..e00dab00 100644 --- a/bun.lock +++ b/bun.lock @@ -98,6 +98,7 @@ }, "devDependencies": { "@babel/core": "^7.26.8", + "@biomejs/biome": "^1.9.4", "@react-native-community/cli": "15.1.3", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.14", @@ -376,6 +377,24 @@ "@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="], "@config-plugins/ffmpeg-kit-react-native": ["@config-plugins/ffmpeg-kit-react-native@9.0.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "expo": "^52" } }, "sha512-04bXwdq7pmUPoGqYV0YGsrW/8Db+TNicn2Hznb5t+Dl740z9QkNGP4A38y1Mdz7mCU2EW0riASwl/JTH+6rBvw=="], diff --git a/package.json b/package.json index 96d743f9..1d4c9fe8 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", "test": "jest --watchAll", - "lint": "expo lint", - "postinstall": "patch-package" + "postinstall": "patch-package", + "lint": "biome format --write ." }, "dependencies": { "@bottom-tabs/react-navigation": "0.8.6", @@ -114,15 +114,16 @@ "@react-native-community/cli": "15.1.3", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.15", "@types/react": "~18.3.12", + "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.0.0", + "@types/uuid": "^10.0.0", "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", "typescript": "~5.7.3", - "@types/lodash": "^4.17.15", - "@types/react-native-vector-icons": "^6.4.18", - "@types/uuid": "^10.0.0" + "@biomejs/biome": "^1.9.4" }, "private": true, "expo": { From defe87debb6f30448ab6a19537b1947235ac27c3 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sun, 16 Mar 2025 17:19:48 +0100 Subject: [PATCH 80/93] fix: disable badge count for sessions --- hooks/useSessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index 738131fe..c0337164 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -29,7 +29,7 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = .filter((s) => s.NowPlayingItem) .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); - Notifications.setBadgeCountAsync(result.length); + // Notifications.setBadgeCountAsync(result.length); return result }, refetchInterval: refetchInterval, From 54423a1267f9db53f8147a058453ddd74dea2eb1 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:57:25 +0100 Subject: [PATCH 81/93] =?UTF-8?q?fix:=20fixed=20app=20crash=20on=20next=20?= =?UTF-8?q?downloaded=20item=20&&=20update=20biome=20schema=20v=E2=80=A6?= =?UTF-8?q?=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biome.json | 8 +- components/video-player/controls/Controls.tsx | 293 ++++++++++++------ 2 files changed, 203 insertions(+), 98 deletions(-) diff --git a/biome.json b/biome.json index f429e9bc..cfee4e6d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "organizeImports": { "enabled": true }, @@ -8,6 +8,10 @@ "enabled": true, "rules": { "recommended": true, + "style": { + "useImportType": "off", + "noNonNullAssertion": "off" + }, "correctness": { "useExhaustiveDependencies": "off" }, "suspicious": { "noExplicitAny": "off" @@ -21,7 +25,7 @@ "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", - "lineWidth": 80, + "lineWidth": 80 }, "javascript": { "formatter": { diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 94914307..d0084720 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -1,43 +1,71 @@ -import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; +import { Text } from "@/components/common/Text"; import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes"; import { useCreditSkipper } from "@/hooks/useCreditSkipper"; import { useHaptic } from "@/hooks/useHaptic"; import { useIntroSkipper } from "@/hooks/useIntroSkipper"; import { useTrickplay } from "@/hooks/useTrickplay"; -import { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; +import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom } from "@/providers/JellyfinProvider"; -import { useSettings, VideoPlayer } from "@/utils/atoms/settings"; +import { VideoPlayer, useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getItemById } from "@/utils/jellyfin/user-library/getItemById"; import { writeToLog } from "@/utils/log"; -import { formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds } from "@/utils/time"; +import { + formatTimeString, + msToTicks, + secondsToMs, + ticksToMs, + ticksToSeconds, +} from "@/utils/time"; import { Ionicons, MaterialIcons } from "@expo/vector-icons"; -import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; +import type { + BaseItemDto, + MediaSourceInfo, +} from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useAtom } from "jotai"; import { debounce } from "lodash"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Platform, TouchableOpacity, useWindowDimensions, View } from "react-native"; +import { + type Dispatch, + type FC, + type MutableRefObject, + type SetStateAction, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { + Platform, + TouchableOpacity, + View, + useWindowDimensions, +} from "react-native"; import { Slider } from "react-native-awesome-slider"; -import { runOnJS, SharedValue, useAnimatedReaction, useSharedValue } from "react-native-reanimated"; +import { + type SharedValue, + runOnJS, + useAnimatedReaction, + useSharedValue, +} from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import AudioSlider from "./AudioSlider"; import BrightnessSlider from "./BrightnessSlider"; -import { ControlProvider } from "./contexts/ControlContext"; -import { VideoProvider } from "./contexts/VideoContext"; -import DropdownView from "./dropdown/DropdownView"; import { EpisodeList } from "./EpisodeList"; import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton"; import SkipButton from "./SkipButton"; -import { useControlsTimeout } from "./useControlsTimeout"; import { VideoTouchOverlay } from "./VideoTouchOverlay"; +import { ControlProvider } from "./contexts/ControlContext"; +import { VideoProvider } from "./contexts/VideoContext"; +import DropdownView from "./dropdown/DropdownView"; +import { useControlsTimeout } from "./useControlsTimeout"; interface Props { item: BaseItemDto; - videoRef: React.MutableRefObject; + videoRef: MutableRefObject; isPlaying: boolean; isSeeking: SharedValue; cacheProgress: SharedValue; @@ -45,7 +73,7 @@ interface Props { isBuffering: boolean; showControls: boolean; ignoreSafeAreas?: boolean; - setIgnoreSafeAreas: React.Dispatch>; + setIgnoreSafeAreas: Dispatch>; enableTrickplay?: boolean; togglePlay: () => void; setShowControls: (shown: boolean) => void; @@ -66,7 +94,7 @@ interface Props { const CONTROLS_TIMEOUT = 4000; -export const Controls: React.FC = ({ +export const Controls: FC = ({ item, seek, startPictureInPicture, @@ -106,13 +134,15 @@ export const Controls: React.FC = ({ const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const { previousItem, nextItem } = useAdjacentItems({ item }); - const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo, prefetchAllTrickplayImages } = useTrickplay( - item, - !offline && enableTrickplay - ); + const { + trickPlayUrl, + calculateTrickplayUrl, + trickplayInfo, + prefetchAllTrickplayImages, + } = useTrickplay(item, !offline && enableTrickplay); const [currentTime, setCurrentTime] = useState(0); - const [remainingTime, setRemainingTime] = useState(Infinity); + const [remainingTime, setRemainingTime] = useState(Number.POSITIVE_INFINITY); const min = useSharedValue(0); const max = useSharedValue(item.RunTimeTicks || 0); @@ -131,7 +161,9 @@ export const Controls: React.FC = ({ progress.value = isVlc ? ticksToMs(item?.UserData?.PlaybackPositionTicks) : item?.UserData?.PlaybackPositionTicks || 0; - max.value = isVlc ? ticksToMs(item.RunTimeTicks || 0) : item.RunTimeTicks || 0; + max.value = isVlc + ? ticksToMs(item.RunTimeTicks || 0) + : item.RunTimeTicks || 0; } }, [item, isVlc]); @@ -141,49 +173,66 @@ export const Controls: React.FC = ({ subtitleIndex: string; }>(); - const { showSkipButton, skipIntro } = useIntroSkipper(offline ? undefined : item.Id, currentTime, seek, play, isVlc); + const { showSkipButton, skipIntro } = useIntroSkipper( + offline ? undefined : item.Id, + currentTime, + seek, + play, + isVlc, + ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( offline ? undefined : item.Id, currentTime, seek, play, - isVlc + isVlc, ); const goToItemCommon = useCallback( (item: BaseItemDto) => { - if (!item || !settings) return; + if (!item || !settings) { + return; + } lightHapticFeedback(); const previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, + subtitleIndex: subtitleIndex + ? Number.parseInt(subtitleIndex) + : undefined, + audioIndex: audioIndex ? Number.parseInt(audioIndex) : undefined, }; const { mediaSource: newMediaSource, audioIndex: defaultAudioIndex, subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings(item, settings, previousIndexes, mediaSource ?? undefined); + } = getDefaultPlaySettings( + item, + settings, + previousIndexes, + mediaSource ?? undefined, + ); const queryParams = new URLSearchParams({ itemId: item.Id ?? "", audioIndex: defaultAudioIndex?.toString() ?? "", subtitleIndex: defaultSubtitleIndex?.toString() ?? "", mediaSourceId: newMediaSource?.Id ?? "", - bitrateValue: bitrateValue.toString(), + bitrateValue: bitrateValue?.toString(), }).toString(); // @ts-expect-error router.replace(`player/direct-player?${queryParams}`); }, - [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router] + [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router], ); const goToPreviousItem = useCallback(() => { - if (!previousItem) return; + if (!previousItem) { + return; + } goToItemCommon(previousItem); }, [previousItem, goToItemCommon]); @@ -198,18 +247,20 @@ export const Controls: React.FC = ({ if (!gotoItem) return; goToItemCommon(gotoItem); }, - [goToItemCommon, api] + [goToItemCommon, api], ); const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { const current = isVlc ? currentProgress : ticksToSeconds(currentProgress); - const remaining = isVlc ? maxValue - currentProgress : ticksToSeconds(maxValue - currentProgress); + const remaining = isVlc + ? maxValue - currentProgress + : ticksToSeconds(maxValue - currentProgress); setCurrentTime(current); setRemainingTime(remaining); }, - [goToNextItem, isVlc] + [goToNextItem, isVlc], ); useAnimatedReaction( @@ -219,11 +270,11 @@ export const Controls: React.FC = ({ isSeeking: isSeeking.value, }), (result) => { - if (result.isSeeking === false) { + if (!result.isSeeking) { runOnJS(updateTimes)(result.progress, result.max); } }, - [updateTimes] + [updateTimes], ); const hideControls = useCallback(() => { @@ -249,7 +300,7 @@ export const Controls: React.FC = ({ }; const handleSliderStart = useCallback(() => { - if (showControls === false) return; + if (!showControls) return; setIsSliding(true); wasPlayingRef.current = isPlaying; @@ -266,9 +317,11 @@ export const Controls: React.FC = ({ setIsSliding(false); seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) { + play(); + } }, - [isVlc] + [isVlc], ); const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 }); @@ -282,7 +335,7 @@ export const Controls: React.FC = ({ const seconds = progressInSeconds % 60; setTime({ hours, minutes, seconds }); }, 3), - [] + [], ); const handleSkipBackward = useCallback(async () => { @@ -296,7 +349,9 @@ export const Controls: React.FC = ({ ? Math.max(0, curr - secondsToMs(settings.rewindSkipTime)) : Math.max(0, ticksToSeconds(curr) - settings.rewindSkipTime); seek(newTime); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) { + play(); + } } } catch (error) { writeToLog("ERROR", "Error seeking video backwards", error); @@ -304,7 +359,9 @@ export const Controls: React.FC = ({ }, [settings, isPlaying, isVlc]); const handleSkipForward = useCallback(async () => { - if (!settings?.forwardSkipTime) return; + if (!settings?.forwardSkipTime) { + return; + } wasPlayingRef.current = isPlaying; lightHapticFeedback(); try { @@ -314,7 +371,7 @@ export const Controls: React.FC = ({ ? curr + secondsToMs(settings.forwardSkipTime) : ticksToSeconds(curr) + settings.forwardSkipTime; seek(Math.max(0, newTime)); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) play(); } } catch (error) { writeToLog("ERROR", "Error seeking video forwards", error); @@ -328,7 +385,9 @@ export const Controls: React.FC = ({ const switchOnEpisodeMode = useCallback(() => { setEpisodeView(true); - if (isPlaying) togglePlay(); + if (isPlaying) { + togglePlay(); + } }, [isPlaying, togglePlay]); const memoizedRenderBubble = useCallback(() => { @@ -360,18 +419,23 @@ export const Controls: React.FC = ({ transform: [{ scale: 1.4 }], borderRadius: 5, }} - className="bg-neutral-800 overflow-hidden" + className='bg-neutral-800 overflow-hidden' > = ({ const onClose = async () => { lightHapticFeedback(); - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ); router.back(); }; return ( - + {episodeView ? ( - setEpisodeView(false)} goToItem={goToItem} /> + setEpisodeView(false)} + goToItem={goToItem} + /> ) : ( <> = ({ position: "absolute", top: settings?.safeAreaInControlsEnabled ? insets.top : 0, right: settings?.safeAreaInControlsEnabled ? insets.right : 0, - width: settings?.safeAreaInControlsEnabled ? screenWidth - insets.left - insets.right : screenWidth, + width: settings?.safeAreaInControlsEnabled + ? screenWidth - insets.left - insets.right + : screenWidth, opacity: showControls ? 1 : 0, }, ]} pointerEvents={showControls ? "auto" : "none"} - className={`flex flex-row w-full pt-2`} + className={"flex flex-row w-full pt-2"} > {!Platform.isTV && ( - + = ({ )} - - {!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && ( - - - - )} + + {!Platform.isTV && + settings.defaultPlayer === VideoPlayer.VLC_4 && ( + + + + )} {item?.Type === "Episode" && !offline && ( { switchOnEpisodeMode(); }} - className="aspect-square flex flex-col rounded-xl items-center justify-center p-2" + className='aspect-square flex flex-col rounded-xl items-center justify-center p-2' > - + )} {previousItem && !offline && ( - + )} {nextItem && !offline && ( - + )} {/* {mediaSource?.TranscodingUrl && ( */} - + {/* )} */} - + @@ -529,9 +610,9 @@ export const Controls: React.FC = ({ }} > = ({ = ({ opacity: showControls ? 1 : 0, }} > - + = ({ bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0, }, ]} - className={`flex flex-col px-2`} + className={"flex flex-col px-2"} onTouchStart={handleControlsInteraction} > = ({ pointerEvents={showControls ? "box-none" : "none"} > {item?.Type === "Episode" && ( - + {`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`} )} - {item?.Name} - {item?.Type === "Movie" && {item?.ProductionYear}} - {item?.Type === "Audio" && {item?.Album}} + {item?.Name} + {item?.Type === "Movie" && ( + + {item?.ProductionYear} + + )} + {item?.Type === "Audio" && ( + {item?.Album} + )} - - - + + + - + = ({ minimumValue={min} maximumValue={max} /> - - + + {formatTimeString(currentTime, isVlc ? "ms" : "s")} - + -{formatTimeString(remainingTime, isVlc ? "ms" : "s")} From 2688e1b981eb18bb274f49cb999d9b2796a764fc Mon Sep 17 00:00:00 2001 From: Ahmed Sbai <30757139+sbaiahmed1@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:57:39 +0100 Subject: [PATCH 82/93] feat: add Polish translation and update language options (#608) --- i18n.ts | 3 + translations/pl.json | 477 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 translations/pl.json diff --git a/i18n.ts b/i18n.ts index 7f35e575..806668c6 100644 --- a/i18n.ts +++ b/i18n.ts @@ -9,6 +9,7 @@ import it from "./translations/it.json"; import ja from "./translations/ja.json"; import tr from "./translations/tr.json"; import nl from "./translations/nl.json"; +import pl from "./translations/pl.json"; import sv from "./translations/sv.json"; import ua from "./translations/ua.json" import zhCN from './translations/zh-CN.json'; @@ -24,6 +25,7 @@ export const APP_LANGUAGES = [ { label: "日本語", value: "ja" }, { label: "Türkçe", value: "tr" }, { label: "Nederlands", value: "nl" }, + { label: 'Polski', value: 'pl' }, { label: "Svenska", value: "sv" }, { label: "Українська", value: "ua" }, { label: "简体中文", value: "zh-CN" }, @@ -40,6 +42,7 @@ i18n.use(initReactI18next).init({ it: { translation: it }, ja: { translation: ja }, nl: { translation: nl }, + pl: { translation: pl }, sv: { translation: sv }, tr: { translation: tr }, ua: { translation: ua }, diff --git a/translations/pl.json b/translations/pl.json new file mode 100644 index 00000000..0f6c31bd --- /dev/null +++ b/translations/pl.json @@ -0,0 +1,477 @@ +{ + "login": { + "username_required": "Nazwa użytkownika jest wymagana", + "error_title": "Błąd", + "login_title": "Zaloguj się", + "login_to_title": "Zaloguj się do", + "username_placeholder": "Nazwa użytkownika", + "password_placeholder": "Hasło", + "login_button": "Zaloguj się", + "quick_connect": "Szybkie połączenie", + "enter_code_to_login": "Wpisz kod {{code}}, aby się zalogować", + "failed_to_initiate_quick_connect": "Nie udało się zainicjować szybkiego połączenia", + "got_it": "Rozumiem", + "connection_failed": "Połączenie nieudane", + "could_not_connect_to_server": "Nie można połączyć się z serwerem. Sprawdź adres URL oraz połączenie sieciowe.", + "an_unexpected_error_occured": "Wystąpił nieoczekiwany błąd", + "change_server": "Zmień serwer", + "invalid_username_or_password": "Nieprawidłowa nazwa użytkownika lub hasło", + "user_does_not_have_permission_to_log_in": "Użytkownik nie ma uprawnień do logowania", + "server_is_taking_too_long_to_respond_try_again_later": "Serwer zbyt długo nie odpowiada – spróbuj ponownie później", + "server_received_too_many_requests_try_again_later": "Serwer otrzymał zbyt wiele żądań – spróbuj ponownie później.", + "there_is_a_server_error": "Wystąpił błąd serwera", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Wystąpił nieoczekiwany błąd. Czy wpisałeś poprawny adres URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "Podaj adres URL serwera Jellyfin", + "server_url_placeholder": "http(s)://twoj-serwer.com", + "connect_button": "Połącz", + "previous_servers": "Poprzednie serwery", + "clear_button": "Wyczyść", + "search_for_local_servers": "Wyszukaj lokalne serwery", + "searching": "Wyszukiwanie...", + "servers": "Serwery" + }, + "home": { + "no_internet": "Brak Internetu", + "no_items": "Brak elementów", + "no_internet_message": "Spokojnie, nadal możesz oglądać\npobrane treści.", + "go_to_downloads": "Przejdź do pobranych", + "oops": "Ups!", + "error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.", + "continue_watching": "Kontynuuj oglądanie", + "next_up": "Następne w kolejce", + "recently_added_in": "Ostatnio dodano w {{libraryName}}", + "suggested_movies": "Sugerowane filmy", + "suggested_episodes": "Sugerowane odcinki", + "intro": { + "welcome_to_streamyfin": "Witamy w Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Darmowy i otwartoźródłowy klient dla Jellyfin.", + "features_title": "Funkcje", + "features_description": "Streamyfin posiada wiele funkcji i integruje się z szeroką gamą oprogramowania (możesz je znaleźć w menu ustawień), w tym:", + "jellyseerr_feature_description": "Połącz się ze swoim serwerem Jellyseerr i zamawiaj filmy bezpośrednio w aplikacji.", + "downloads_feature_title": "Pobieranie", + "downloads_feature_description": "Pobieraj filmy oraz seriale do oglądania offline. Użyj domyślnej metody lub zainstaluj serwer do optymalizacji, aby pobierać pliki w tle.", + "chromecast_feature_description": "Przesyłaj filmy i seriale na urządzenia Chromecast.", + "centralised_settings_plugin_title": "Scentralizowana wtyczka ustawień", + "centralised_settings_plugin_description": "Konfiguruj ustawienia z jednego miejsca na serwerze Jellyfin. Wszystkie ustawienia klientów dla wszystkich użytkowników będą synchronizowane automatycznie.", + "done_button": "Gotowe", + "go_to_settings_button": "Przejdź do ustawień", + "read_more": "Czytaj więcej" + }, + "settings": { + "settings_title": "Ustawienia", + "log_out_button": "Wyloguj się", + "user_info": { + "user_info_title": "Informacje o użytkowniku", + "user": "Użytkownik", + "server": "Serwer", + "token": "Token", + "app_version": "Wersja aplikacji" + }, + "quick_connect": { + "quick_connect_title": "Szybkie połączenie", + "authorize_button": "Autoryzuj szybkie połączenie", + "enter_the_quick_connect_code": "Wpisz kod szybkiego połączenia...", + "success": "Sukces", + "quick_connect_autorized": "Szybkie połączenie autoryzowane", + "error": "Błąd", + "invalid_code": "Nieprawidłowy kod", + "authorize": "Autoryzuj" + }, + "media_controls": { + "media_controls_title": "Sterowanie multimediami", + "forward_skip_length": "Długość przewijania do przodu", + "rewind_length": "Długość przewijania do tyłu", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Ustaw ścieżkę audio z poprzedniego elementu", + "audio_language": "Język audio", + "audio_hint": "Wybierz domyślny język audio.", + "none": "Brak", + "language": "Język" + }, + "subtitles": { + "subtitle_title": "Napisy", + "subtitle_language": "Język napisów", + "subtitle_mode": "Tryb napisów", + "set_subtitle_track": "Ustaw ścieżkę napisów z poprzedniego elementu", + "subtitle_size": "Rozmiar napisów", + "subtitle_hint": "Skonfiguruj preferencje dotyczące napisów.", + "none": "Brak", + "language": "Język", + "loading": "Ładowanie", + "modes": { + "Default": "Domyślny", + "Smart": "Inteligentny", + "Always": "Zawsze", + "None": "Brak", + "OnlyForced": "Tylko wymuszone" + } + }, + "other": { + "other_title": "Inne", + "follow_device_orientation": "Podążaj za orientacją urządzenia", + "video_orientation": "Orientacja wideo", + "orientation": "Orientacja", + "orientations": { + "DEFAULT": "Domyślna", + "ALL": "Wszystkie", + "PORTRAIT": "Pionowa", + "PORTRAIT_UP": "Pionowa w górę", + "PORTRAIT_DOWN": "Pionowa w dół", + "LANDSCAPE": "Pozioma", + "LANDSCAPE_LEFT": "Pozioma w lewo", + "LANDSCAPE_RIGHT": "Pozioma w prawo", + "OTHER": "Inna", + "UNKNOWN": "Nieznana" + }, + "safe_area_in_controls": "Bezpieczny obszar w kontrolkach", + "video_player": "Odtwarzacz wideo", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperymentalny + PiP)" + }, + "show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu", + "hide_libraries": "Ukryj biblioteki", + "select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.", + "disable_haptic_feedback": "Wyłącz wibracje", + "default_quality": "Domyślna jakość" + }, + "downloads": { + "downloads_title": "Pobieranie", + "download_method": "Metoda pobierania", + "remux_max_download": "Maksymalne pobieranie remux", + "auto_download": "Automatyczne pobieranie", + "optimized_versions_server": "Serwer zoptymalizowanych wersji", + "save_button": "Zapisz", + "optimized_server": "Serwer zoptymalizowany", + "optimized": "Zoptymalizowany", + "default": "Domyślny", + "optimized_version_hint": "Podaj adres URL dla serwera optymalizującego. Adres powinien zawierać http lub https oraz opcjonalnie port.", + "read_more_about_optimized_server": "Dowiedz się więcej o serwerze optymalizującym.", + "url": "URL", + "server_url_placeholder": "http(s)://domena.org:port" + }, + "plugins": { + "plugins_title": "Wtyczki", + "jellyseerr": { + "jellyseerr_warning": "Ta integracja jest na wczesnym etapie. Należy oczekiwać zmian.", + "server_url": "URL serwera", + "server_url_hint": "Przykład: http(s)://twoja-nazwa.url\n(dodaj port, jeśli jest wymagany)", + "server_url_placeholder": "Adres URL Jellyseerr...", + "password": "Hasło", + "password_placeholder": "Wpisz hasło użytkownika Jellyfin {{username}}", + "save_button": "Zapisz", + "clear_button": "Wyczyść", + "login_button": "Zaloguj", + "total_media_requests": "Łączna liczba próśb o media", + "movie_quota_limit": "Limit zapytań o filmy", + "movie_quota_days": "Okres limitu (dni) dla filmów", + "tv_quota_limit": "Limit zapytań o seriale", + "tv_quota_days": "Okres limitu (dni) dla seriali", + "reset_jellyseerr_config_button": "Resetuj konfigurację Jellyseerr", + "unlimited": "Bez limitu", + "plus_n_more": "+{{n}} więcej", + "order_by": { + "DEFAULT": "Domyślny", + "VOTE_COUNT_AND_AVERAGE": "Liczba głosów i średnia", + "POPULARITY": "Popularność" + } + }, + "marlin_search": { + "enable_marlin_search": "Włącz wyszukiwanie Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://domena.org:port", + "marlin_search_hint": "Podaj adres URL serwera Marlin. Adres powinien zawierać http lub https oraz opcjonalnie port.", + "read_more_about_marlin": "Dowiedz się więcej o Marlin.", + "save_button": "Zapisz", + "toasts": { + "saved": "Zapisano" + } + } + }, + "storage": { + "storage_title": "Pamięć", + "app_usage": "Aplikacja {{usedSpace}}%", + "device_usage": "Urządzenie {{availableSpace}}%", + "size_used": "{{used}} z {{total}} wykorzystane", + "delete_all_downloaded_files": "Usuń wszystkie pobrane pliki" + }, + "intro": { + "show_intro": "Pokaż wprowadzenie", + "reset_intro": "Zresetuj wprowadzenie" + }, + "logs": { + "logs_title": "Logi", + "no_logs_available": "Brak dostępnych logów", + "delete_all_logs": "Usuń wszystkie logi" + }, + "languages": { + "title": "Języki", + "app_language": "Język aplikacji", + "app_language_description": "Wybierz język aplikacji.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Błąd podczas usuwania plików", + "background_downloads_enabled": "Pobieranie w tle włączone", + "background_downloads_disabled": "Pobieranie w tle wyłączone", + "connected": "Połączono", + "could_not_connect": "Nie udało się połączyć", + "invalid_url": "Nieprawidłowy URL" + } + }, + "sessions": { + "title": "Sesje", + "no_active_sessions": "Brak aktywnych sesji" + }, + "downloads": { + "downloads_title": "Pobrane", + "tvseries": "Seriale", + "movies": "Filmy", + "queue": "Kolejka", + "queue_hint": "Kolejka i pobierania zostaną utracone po ponownym uruchomieniu aplikacji", + "no_items_in_queue": "Brak elementów w kolejce", + "no_downloaded_items": "Brak pobranych elementów", + "delete_all_movies_button": "Usuń wszystkie filmy", + "delete_all_tvseries_button": "Usuń wszystkie seriale", + "delete_all_button": "Usuń wszystko", + "active_download": "Aktywne pobieranie", + "no_active_downloads": "Brak aktywnych pobrań", + "active_downloads": "Aktywne pobrania", + "new_app_version_requires_re_download": "Nowa wersja aplikacji wymaga ponownego pobrania", + "new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.", + "back": "Wstecz", + "delete": "Usuń", + "something_went_wrong": "Coś poszło nie tak", + "could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać URL transmisji z Jellyfin", + "eta": "Szacowany czas: {{eta}}", + "methods": "Metody", + "toasts": { + "you_are_not_allowed_to_download_files": "Nie masz uprawnień do pobierania plików.", + "deleted_all_movies_successfully": "Wszystkie filmy zostały pomyślnie usunięte!", + "failed_to_delete_all_movies": "Nie udało się usunąć wszystkich filmów", + "deleted_all_tvseries_successfully": "Wszystkie seriale zostały pomyślnie usunięte!", + "failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali", + "download_cancelled": "Pobieranie anulowane", + "could_not_cancel_download": "Nie udało się anulować pobierania", + "download_completed": "Pobieranie zakończone", + "download_started_for": "Rozpoczęto pobieranie: {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} jest gotowe do pobrania", + "download_stated_for_item": "Rozpoczęto pobieranie: {{item}}", + "download_failed_for_item": "Pobieranie nie powiodło się dla {{item}} – {{error}}", + "download_completed_for_item": "Zakończono pobieranie: {{item}}", + "queued_item_for_optimization": "Dodano do kolejki optymalizacji: {{item}}", + "failed_to_start_download_for_item": "Nie udało się rozpocząć pobierania: {{item}}: {{message}}", + "server_responded_with_status_code": "Serwer zwrócił status {{statusCode}}", + "no_response_received_from_server": "Serwer nie zwrócił odpowiedzi", + "error_setting_up_the_request": "Błąd podczas konfiguracji żądania", + "failed_to_start_download_for_item_unexpected_error": "Nie udało się rozpocząć pobierania dla {{item}}: Nieoczekiwany błąd", + "all_files_folders_and_jobs_deleted_successfully": "Wszystkie pliki, foldery i zadania zostały pomyślnie usunięte", + "an_error_occured_while_deleting_files_and_jobs": "Wystąpił błąd podczas usuwania plików i zadań", + "go_to_downloads": "Przejdź do pobranych" + } + } + }, + "search": { + "search_here": "Szukaj tutaj...", + "search": "Szukaj...", + "x_items": "{{count}} elementów", + "library": "Biblioteka", + "discover": "Odkrywaj", + "no_results": "Brak wyników", + "no_results_found_for": "Nie znaleziono wyników dla", + "movies": "Filmy", + "series": "Seriale", + "episodes": "Odcinki", + "collections": "Kolekcje", + "actors": "Aktorzy", + "request_movies": "Zamów filmy", + "request_series": "Zamów seriale", + "recently_added": "Ostatnio dodane", + "recent_requests": "Ostatnie zamówienia", + "plex_watchlist": "Plex Watchlist", + "trending": "Popularne", + "popular_movies": "Popularne filmy", + "movie_genres": "Gatunki filmowe", + "upcoming_movies": "Nadchodzące filmy", + "studios": "Studia", + "popular_tv": "Popularne seriale", + "tv_genres": "Gatunki seriali", + "upcoming_tv": "Nadchodzące seriale", + "networks": "Sieci", + "tmdb_movie_keyword": "Słowo kluczowe filmu (TMDB)", + "tmdb_movie_genre": "Gatunek filmu (TMDB)", + "tmdb_tv_keyword": "Słowo kluczowe serialu (TMDB)", + "tmdb_tv_genre": "Gatunek serialu (TMDB)", + "tmdb_search": "Wyszukiwanie TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Sieć TMDB", + "tmdb_movie_streaming_services": "Usługi streamingowe filmów (TMDB)", + "tmdb_tv_streaming_services": "Usługi streamingowe seriali (TMDB)" + }, + "library": { + "no_items_found": "Nie znaleziono elementów", + "no_results": "Brak wyników", + "no_libraries_found": "Nie znaleziono bibliotek", + "item_types": { + "movies": "filmy", + "series": "seriale", + "boxsets": "zestawy", + "items": "elementy" + }, + "options": { + "display": "Wyświetlanie", + "row": "Wiersz", + "list": "Lista", + "image_style": "Styl obrazu", + "poster": "Plakat", + "cover": "Okładka", + "show_titles": "Pokaż tytuły", + "show_stats": "Pokaż statystyki" + }, + "filters": { + "genres": "Gatunki", + "years": "Lata", + "sort_by": "Sortuj według", + "sort_order": "Kolejność sortowania", + "asc": "Rosnąco", + "desc": "Malejąco", + "tags": "Tagi" + } + }, + "favorites": { + "series": "Seriale", + "movies": "Filmy", + "episodes": "Odcinki", + "videos": "Filmy wideo", + "boxsets": "Zestawy", + "playlists": "Playlisty", + "noDataTitle": "Brak ulubionych jeszcze", + "noData": "Dodaj elementy do ulubionych, aby zobaczyć je tutaj dla szybkiego dostępu." + }, + "custom_links": { + "no_links": "Brak odnośników" + }, + "player": { + "error": "Błąd", + "failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia", + "an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.", + "client_error": "Błąd klienta", + "could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta", + "message_from_server": "Wiadomość z serwera: {{message}}", + "video_has_finished_playing": "Wideo zostało odtworzone do końca!", + "no_video_source": "Brak źródła wideo...", + "next_episode": "Następny odcinek", + "refresh_tracks": "Odśwież ścieżki", + "subtitle_tracks": "Ścieżki napisów:", + "audio_tracks": "Ścieżki audio:", + "playback_state": "Stan odtwarzania:", + "no_data_available": "Brak dostępnych danych", + "index": "Indeks:" + }, + "item_card": { + "next_up": "Następne", + "no_items_to_display": "Brak elementów do wyświetlenia", + "cast_and_crew": "Obsada i ekipa", + "series": "Serial", + "seasons": "Sezony", + "season": "Sezon", + "no_episodes_for_this_season": "Brak odcinków w tym sezonie", + "overview": "Opis", + "more_with": "Więcej z {{name}}", + "similar_items": "Podobne elementy", + "no_similar_items_found": "Nie znaleziono podobnych elementów", + "video": "Wideo", + "more_details": "Więcej szczegółów", + "quality": "Jakość", + "audio": "Audio", + "subtitles": "Napisy", + "show_more": "Pokaż więcej", + "show_less": "Pokaż mniej", + "appeared_in": "Wystąpił w", + "could_not_load_item": "Nie udało się wczytać elementu", + "none": "Brak", + "download": { + "download_season": "Pobierz sezon", + "download_series": "Pobierz serial", + "download_episode": "Pobierz odcinek", + "download_movie": "Pobierz film", + "download_x_item": "Pobierz {{item_count}} elementów", + "download_button": "Pobierz", + "using_optimized_server": "Używanie serwera zoptymalizowanego", + "using_default_method": "Używanie metody domyślnej" + } + }, + "live_tv": { + "next": "Następny", + "previous": "Poprzedni", + "live_tv": "Telewizja na żywo", + "coming_soon": "Już wkrótce", + "on_now": "Teraz na żywo", + "shows": "Programy", + "movies": "Filmy", + "sports": "Sport", + "for_kids": "Dla dzieci", + "news": "Wiadomości" + }, + "jellyseerr": { + "confirm": "Potwierdź", + "cancel": "Anuluj", + "yes": "Tak", + "whats_wrong": "Co jest nie tak?", + "issue_type": "Typ problemu", + "select_an_issue": "Wybierz problem", + "types": "Typy", + "describe_the_issue": "(opcjonalnie) Opisz problem...", + "submit_button": "Zgłoś", + "report_issue_button": "Zgłoś problem", + "request_button": "Poproś", + "are_you_sure_you_want_to_request_all_seasons": "Czy na pewno chcesz zamówić wszystkie sezony?", + "failed_to_login": "Logowanie nie powiodło się", + "cast": "Obsada", + "details": "Szczegóły", + "status": "Status", + "original_title": "Oryginalny tytuł", + "series_type": "Typ serialu", + "release_dates": "Daty premiery", + "first_air_date": "Data pierwszej emisji", + "next_air_date": "Data następnej emisji", + "revenue": "Przychód", + "budget": "Budżet", + "original_language": "Oryginalny język", + "production_country": "Kraj produkcji", + "studios": "Studia", + "network": "Sieć", + "currently_streaming_on": "Aktualnie dostępne w streamingu na", + "advanced": "Zaawansowane", + "request_as": "Poproś jako", + "tags": "Tagi", + "quality_profile": "Profil jakości", + "root_folder": "Folder główny", + "season_all": "Sezon (wszystkie)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "{{episode_number}} odcinków", + "born": "Urodzony", + "appearances": "Występy", + "toasts": { + "jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0", + "jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.", + "failed_to_test_jellyseerr_server_url": "Nie udało się przetestować adresu URL Jellyseerr", + "issue_submitted": "Zgłoszenie zostało przesłane!", + "requested_item": "Poproszono o {{item}}!", + "you_dont_have_permission_to_request": "Nie masz uprawnień, aby złożyć zamówienie!", + "something_went_wrong_requesting_media": "Coś poszło nie tak podczas zamawiania materiałów!" + } + }, + "tabs": { + "home": "Strona główna", + "search": "Szukaj", + "library": "Biblioteka", + "custom_links": "Niestandardowe odnośniki", + "favorites": "Ulubione" + } +} From 92513e234f685877b5ef6932e5feaf1f68f37590 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sun, 16 Mar 2025 18:01:12 +0100 Subject: [PATCH 83/93] chore: Apply linting rules and add git hok (#611) Co-authored-by: Fredrik Burmester --- .husky/pre-commit | 1 + .vscode/settings.json | 44 +- app/(auth)/(tabs)/(custom-links)/_layout.tsx | 6 +- app/(auth)/(tabs)/(custom-links)/index.tsx | 26 +- app/(auth)/(tabs)/(favorites)/_layout.tsx | 4 +- app/(auth)/(tabs)/(favorites)/index.tsx | 4 +- app/(auth)/(tabs)/(home)/_layout.tsx | 44 +- .../(tabs)/(home)/downloads/[seriesId].tsx | 34 +- app/(auth)/(tabs)/(home)/downloads/index.tsx | 114 ++- app/(auth)/(tabs)/(home)/intro/page.tsx | 66 +- app/(auth)/(tabs)/(home)/sessions/index.tsx | 201 ++-- app/(auth)/(tabs)/(home)/settings.tsx | 24 +- .../(home)/settings/hide-libraries/page.tsx | 16 +- .../(home)/settings/jellyseerr/page.tsx | 4 +- .../(tabs)/(home)/settings/logs/page.tsx | 14 +- .../(home)/settings/marlin-search/page.tsx | 50 +- .../(home)/settings/optimized-server/page.tsx | 14 +- .../actors/[actorId].tsx | 20 +- .../collections/[collectionId].tsx | 67 +- .../items/page.tsx | 37 +- .../jellyseerr/company/[companyId].tsx | 88 +- .../jellyseerr/genre/[genreId].tsx | 93 +- .../jellyseerr/page.tsx | 149 +-- .../jellyseerr/person/[personId].tsx | 82 +- .../livetv/_layout.tsx | 17 +- .../livetv/channels.tsx | 8 +- .../livetv/guide.tsx | 28 +- .../livetv/programs.tsx | 20 +- .../livetv/recordings.tsx | 4 +- .../series/[id].tsx | 23 +- app/(auth)/(tabs)/(libraries)/[libraryId].tsx | 80 +- app/(auth)/(tabs)/(libraries)/_layout.tsx | 42 +- app/(auth)/(tabs)/(libraries)/index.tsx | 22 +- app/(auth)/(tabs)/(search)/_layout.tsx | 14 +- app/(auth)/(tabs)/(search)/index.tsx | 176 ++-- app/(auth)/(tabs)/_layout.tsx | 26 +- app/(auth)/player/_layout.tsx | 6 +- app/(auth)/player/direct-player.tsx | 111 +- app/+html.tsx | 12 +- app/+not-found.tsx | 4 +- app/_layout.tsx | 186 ++-- app/login.tsx | 89 +- augmentations/api.ts | 18 +- augmentations/mmkv.ts | 17 +- augmentations/number.ts | 10 +- augmentations/string.ts | 14 +- babel.config.js | 2 +- biome.json | 4 + components/AddToFavorites.tsx | 13 +- components/AudioTrackSelector.tsx | 22 +- components/Badge.tsx | 4 +- components/BitrateSelector.tsx | 30 +- components/Button.tsx | 9 +- components/Chromecast.tsx | 16 +- components/ContinueWatchingPoster.tsx | 18 +- components/DownloadItem.tsx | 73 +- components/GenreTags.tsx | 52 +- components/ItemCardText.tsx | 18 +- components/ItemContent.tsx | 52 +- components/ItemHeader.tsx | 26 +- components/ItemTechnicalDetails.tsx | 127 +-- components/JellyfinServerDiscovery.tsx | 18 +- components/Loader.tsx | 2 +- components/MediaSourceSelector.tsx | 20 +- components/MoreMoviesWithActor.tsx | 22 +- components/OverviewText.tsx | 14 +- components/ParallaxPage.tsx | 35 +- components/PlatformBlurView.tsx | 4 +- components/PlayButton.tsx | 116 +-- components/PlayButton.tv.tsx | 56 +- components/PlayedStatus.tsx | 12 +- components/PreviousServersList.tsx | 9 +- components/ProgressCircle.tsx | 4 +- components/Ratings.tsx | 53 +- components/RoundButton.tsx | 6 +- components/SimilarItems.tsx | 23 +- components/SubtitleTrackSelector.tsx | 20 +- components/ThemedText.tsx | 2 +- components/WatchedIndicator.tsx | 6 +- components/__tests__/ThemedText-test.tsx | 10 +- components/_template.tsx | 2 +- components/common/ColumnItem.tsx | 4 +- components/common/Dropdown.tsx | 28 +- components/common/HeaderBackButton.tsx | 26 +- components/common/HorrizontalScroll.tsx | 20 +- .../common/InfiniteHorrizontalScroll.tsx | 18 +- components/common/Input.tsx | 37 +- components/common/ItemImage.tsx | 16 +- components/common/JellyseerrItemRouter.tsx | 51 +- components/common/LargePoster.tsx | 10 +- components/common/Text.tsx | 6 +- components/common/TouchableItemRouter.tsx | 34 +- components/common/VerticalSkeleton.tsx | 12 +- components/downloads/ActiveDownloads.tsx | 58 +- components/downloads/DownloadSize.tsx | 9 +- components/downloads/EpisodeCard.tsx | 43 +- components/downloads/MovieCard.tsx | 21 +- components/downloads/SeriesCard.tsx | 56 +- components/filters/FilterButton.tsx | 10 +- components/filters/FilterSheet.tsx | 44 +- components/filters/ResetFiltersButton.tsx | 6 +- components/home/Favorites.tsx | 280 +++--- components/home/LargeMovieCarousel.tsx | 16 +- components/home/ScrollingCollectionList.tsx | 28 +- components/inputs/Stepper.tsx | 36 +- components/jellyseerr/Cast.tsx | 16 +- components/jellyseerr/DetailFacts.tsx | 92 +- components/jellyseerr/JellyseerrIndexPage.tsx | 111 +- components/jellyseerr/JellyseerrMediaIcon.tsx | 59 +- .../jellyseerr/JellyseerrStatusIcon.tsx | 66 +- components/jellyseerr/ParallaxSlideShow.tsx | 77 +- components/jellyseerr/PersonPoster.tsx | 40 +- components/jellyseerr/RequestModal.tsx | 453 +++++---- .../jellyseerr/discover/CompanySlide.tsx | 19 +- components/jellyseerr/discover/Discover.tsx | 59 +- .../jellyseerr/discover/GenericSlideCard.tsx | 55 +- components/jellyseerr/discover/GenreSlide.tsx | 21 +- .../jellyseerr/discover/MovieTvSlide.tsx | 47 +- .../discover/RecentRequestsSlide.tsx | 69 +- components/jellyseerr/discover/Slide.tsx | 35 +- components/library/LibraryItemCard.tsx | 36 +- components/list/ListGroup.tsx | 18 +- components/list/ListItem.tsx | 24 +- components/livetv/HourHeader.tsx | 6 +- components/livetv/LiveTVGuideRow.tsx | 10 +- components/medialists/MediaListSection.tsx | 20 +- components/movies/MoviesTitleHeader.tsx | 8 +- components/navigation/TabBarIcon.tsx | 4 +- components/posters/EpisodePoster.tsx | 12 +- components/posters/ItemPoster.tsx | 18 +- components/posters/JellyseerrPoster.tsx | 171 ++-- components/posters/MoviePoster.tsx | 10 +- components/posters/ParentPoster.tsx | 8 +- components/posters/Poster.tsx | 8 +- components/posters/SeriesPoster.tsx | 8 +- components/search/LoadingSkeleton.tsx | 24 +- components/search/SearchItemWrapper.tsx | 21 +- components/series/CastAndCrew.tsx | 23 +- components/series/CurrentSeries.tsx | 18 +- components/series/EpisodeTitleHeader.tsx | 18 +- components/series/JellyseerrSeasons.tsx | 210 ++-- components/series/NextItemButton.tsx | 16 +- components/series/NextUp.tsx | 22 +- components/series/SeasonDropdown.tsx | 20 +- components/series/SeasonEpisodesCarousel.tsx | 18 +- components/series/SeasonPicker.tsx | 70 +- components/series/SeriesActions.tsx | 12 +- components/series/SeriesHeader.tsx | 20 +- components/settings/AppLanguageSelector.tsx | 16 +- components/settings/AudioToggles.tsx | 26 +- components/settings/ChromecastSettings.tsx | 2 +- components/settings/Dashboard.tsx | 6 +- components/settings/DisabledSetting.tsx | 26 +- components/settings/DownloadSettings.tsx | 30 +- components/settings/HomeIndex.tsx | 64 +- components/settings/Jellyseerr.tsx | 54 +- components/settings/MediaContext.tsx | 28 +- components/settings/MediaToggles.tsx | 30 +- components/settings/OptimizedServerForm.tsx | 22 +- components/settings/OtherSettings.tsx | 92 +- components/settings/PluginSettings.tsx | 9 +- components/settings/QuickConnect.tsx | 39 +- components/settings/StorageSettings.tsx | 32 +- components/settings/SubtitleToggles.tsx | 36 +- components/settings/UserInfo.tsx | 34 +- components/stacks/NestedTabPageStack.tsx | 4 +- .../video-player/controls/AudioSlider.tsx | 43 +- .../controls/BrightnessSlider.tsx | 8 +- .../video-player/controls/EpisodeList.tsx | 36 +- .../controls/NextEpisodeCountDownButton.tsx | 23 +- .../video-player/controls/SkipButton.tsx | 8 +- .../video-player/controls/SliderScrubbter.tsx | 36 +- .../controls/contexts/ControlContext.tsx | 7 +- .../controls/contexts/VideoContext.tsx | 83 +- .../controls/dropdown/DropdownView.tsx | 51 +- components/video-player/controls/types.ts | 2 +- .../video-player/controls/useTapDetection.tsx | 4 +- components/vlc/VideoDebugInfo.tsx | 26 +- constants/Languages.ts | 2 +- hooks/useAdjacentEpisodes.ts | 4 +- hooks/useControlsVisibility.ts | 4 +- hooks/useCreditSkipper.ts | 12 +- hooks/useDefaultPlaySettings.ts | 12 +- hooks/useDownloadedFileOpener.ts | 4 +- hooks/useFavorite.ts | 6 +- hooks/useHaptic.ts | 12 +- hooks/useImageColors.ts | 68 +- hooks/useImageStorage.ts | 2 +- hooks/useIntroSkipper.ts | 12 +- hooks/useJellyfinDiscovery.tsx | 4 +- hooks/useJellyseerr.ts | 227 +++-- hooks/useMarkAsPlayed.ts | 42 +- hooks/useOrientation.ts | 6 +- hooks/useRemuxHlsToMp4.ts | 32 +- hooks/useSessions.ts | 23 +- hooks/useTrickplay.ts | 4 +- hooks/useWebsockets.ts | 8 +- i18n.ts | 10 +- modules/VlcPlayerView.tsx | 22 +- modules/index.ts | 12 +- modules/vlc-player-3/src/VlcPlayer3Module.ts | 4 +- modules/vlc-player/src/VlcPlayerModule.ts | 4 +- package.json | 15 +- plugins/withAndroidManifest.js | 21 +- plugins/withChangeNativeAndroidTextToWhite.js | 9 +- plugins/withGradleProperties.js | 29 +- plugins/withRNBackgroundDownloader.js | 6 +- plugins/withTrustLocalCerts.js | 4 +- providers/DownloadProvider.tsx | 99 +- providers/DownloadProvider.tv.tsx | 13 +- providers/JellyfinProvider.tsx | 53 +- providers/JobQueueProvider.tsx | 3 +- providers/PlaySettingsProvider.tsx | 17 +- providers/WebSocketProvider.tsx | 17 +- scripts/symlink-native-dirs.js | 10 +- translations/de.json | 946 ++++++++--------- translations/en.json | 950 +++++++++--------- translations/es.json | 942 ++++++++--------- translations/fr.json | 942 ++++++++--------- translations/it.json | 942 ++++++++--------- translations/ja.json | 940 ++++++++--------- translations/nl.json | 942 ++++++++--------- translations/sv.json | 64 +- translations/tr.json | 940 ++++++++--------- translations/zh-CN.json | 940 ++++++++--------- translations/zh-TW.json | 940 ++++++++--------- types.d.ts | 8 +- utils/OrientationLockConverter.ts | 2 +- utils/_jellyseerr/useJellyseerrCanRequest.ts | 38 +- utils/atoms/filters.ts | 8 +- utils/atoms/orientation.ts | 2 +- utils/atoms/primaryColor.ts | 12 +- utils/atoms/queue.ts | 19 +- utils/atoms/settings.ts | 135 ++- utils/background-tasks.ts | 5 +- utils/bitrate.ts | 4 +- utils/collectionTypeToItemType.ts | 2 +- utils/device.ts | 2 +- utils/download.ts | 4 +- utils/eventBus.ts | 2 +- utils/getItemImage.ts | 6 +- utils/hls/parseM3U8ForSubtitles.ts | 2 +- utils/jellyfin/getDefaultPlaySettings.ts | 16 +- utils/jellyfin/image/getBackdropUrl.ts | 4 +- utils/jellyfin/image/getLogoImageUrlById.ts | 4 +- .../image/getParentBackdropImageUrl.ts | 4 +- utils/jellyfin/image/getPrimaryImageUrl.ts | 4 +- .../jellyfin/image/getPrimaryImageUrlById.ts | 2 +- .../image/getPrimaryParentImageUrl.ts | 4 +- utils/jellyfin/jellyfin.ts | 4 +- utils/jellyfin/media/getPlaybackUrl.ts | 2 +- utils/jellyfin/media/getStreamUrl.ts | 10 +- utils/jellyfin/playstate/markAsNotPlayed.ts | 4 +- utils/jellyfin/playstate/markAsPlayed.ts | 10 +- .../playstate/reportPlaybackProgress.ts | 18 +- utils/jellyfin/session/capabilities.ts | 8 +- utils/jellyfin/tvshows/nextUp.ts | 4 +- utils/jellyfin/user-library/getItemById.ts | 4 +- .../jellyfin/user-library/getUserItemData.ts | 4 +- utils/log.tsx | 11 +- utils/optimize-server.ts | 10 +- utils/profiles/chromecast.ts | 2 +- utils/profiles/chromecasth265.ts | 2 +- utils/store.ts | 2 +- utils/streamRanker.ts | 20 +- utils/textTools.ts | 2 +- utils/time.ts | 6 +- utils/useReactNavigationQuery.ts | 12 +- 268 files changed, 9197 insertions(+), 8394 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..c27d8893 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +lint-staged diff --git a/.vscode/settings.json b/.vscode/settings.json index c7cdc61f..b200b485 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,24 @@ { - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "prettier.printWidth": 120, - "[swift]": { - "editor.defaultFormatter": "sswg.swift-lang" - }, - "editor.formatOnSave": true, - "editor.defaultFormatter": "biomejs.biome", - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - } + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "prettier.printWidth": 120, + "[swift]": { + "editor.defaultFormatter": "sswg.swift-lang" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + } } diff --git a/app/(auth)/(tabs)/(custom-links)/_layout.tsx b/app/(auth)/(tabs)/(custom-links)/_layout.tsx index c270b95d..a580146f 100644 --- a/app/(auth)/(tabs)/(custom-links)/_layout.tsx +++ b/app/(auth)/(tabs)/(custom-links)/_layout.tsx @@ -1,13 +1,13 @@ -import {Stack} from "expo-router"; -import { Platform } from "react-native"; +import { Stack } from "expo-router"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function CustomMenuLayout() { const { t } = useTranslation(); return ( { try { const response = await api?.axiosInstance.get( - api?.basePath + "/web/config.json" + api?.basePath + "/web/config.json", ); const config = response?.data; @@ -46,7 +46,7 @@ export default function menuLinks() { }, []); return ( } + iconAfter={} /> )} @@ -76,8 +76,10 @@ export default function menuLinks() { /> )} ListEmptyComponent={ - - {t("custom_links.no_links")} + + + {t("custom_links.no_links")} + } /> diff --git a/app/(auth)/(tabs)/(favorites)/_layout.tsx b/app/(auth)/(tabs)/(favorites)/_layout.tsx index b408eab6..ba0844d8 100644 --- a/app/(auth)/(tabs)/(favorites)/_layout.tsx +++ b/app/(auth)/(tabs)/(favorites)/_layout.tsx @@ -1,14 +1,14 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { Stack } from "expo-router"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function SearchLayout() { const { t } = useTranslation(); return ( } @@ -28,7 +28,7 @@ export default function favorites() { paddingBottom: 16, }} > - + diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index dee024fd..0d533ac9 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,12 +1,12 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; -import { Ionicons, Feather } from "@expo/vector-icons"; +import { Feather, Ionicons } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; -import { Platform, TouchableOpacity, View } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform, TouchableOpacity, View } from "react-native"; const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); -import { useAtom } from "jotai"; +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; import { userAtom } from "@/providers/JellyfinProvider"; -import { useSessions, useSessionsProps } from "@/hooks/useSessions"; +import { useAtom } from "jotai"; export default function IndexLayout() { const router = useRouter(); @@ -16,7 +16,7 @@ export default function IndexLayout() { return ( ( - + {!Platform.isTV && ( <> - {user && user.Policy?.IsAdministrator && ( - - )} + {user && user.Policy?.IsAdministrator && } )} @@ -43,61 +41,61 @@ export default function IndexLayout() { }} /> ))} { router.push("/(auth)/settings"); }} > - + ); }; @@ -145,9 +143,9 @@ const SessionsButton = () => { router.push("/(auth)/sessions"); }} > - + diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx index e9c95657..59d111f4 100644 --- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx +++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx @@ -1,16 +1,16 @@ import { Text } from "@/components/common/Text"; -import { useDownload } from "@/providers/DownloadProvider"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ScrollView, TouchableOpacity, View, Alert } from "react-native"; import { EpisodeCard } from "@/components/downloads/EpisodeCard"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { SeasonDropdown, - SeasonIndexState, + type SeasonIndexState, } from "@/components/series/SeasonDropdown"; +import { useDownload } from "@/providers/DownloadProvider"; import { storage } from "@/utils/mmkv"; import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { router, useLocalSearchParams, useNavigation } from "expo-router"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; export default function page() { const navigation = useNavigation(); @@ -21,7 +21,7 @@ export default function page() { }; const [seasonIndexState, setSeasonIndexState] = useState( - {} + {}, ); const { downloadedFiles, deleteItems } = useDownload(); @@ -31,7 +31,7 @@ export default function page() { downloadedFiles ?.filter((f) => f.item.SeriesId == seriesId) ?.sort( - (a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber! + (a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!, ) || [] ); } catch { @@ -64,7 +64,7 @@ export default function page() { () => Object.values(groupBySeason)?.[0]?.ParentIndexNumber ?? series?.[0]?.item?.ParentIndexNumber, - [groupBySeason] + [groupBySeason], ); useEffect(() => { @@ -92,14 +92,14 @@ export default function page() { onPress: () => deleteItems(groupBySeason), style: "destructive", }, - ] + ], ); }, [groupBySeason]); return ( - + {series.length > 0 && ( - + s.item)} @@ -112,17 +112,17 @@ export default function page() { })); }} /> - - {groupBySeason.length} + + {groupBySeason.length} - + - + )} - + {groupBySeason.map((episode, index) => ( ))} diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 51991f1b..52c94c06 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -1,28 +1,28 @@ +import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { ActiveDownloads } from "@/components/downloads/ActiveDownloads"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; -import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; +import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider"; import { queueAtom } from "@/utils/atoms/queue"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; +import { writeToLog } from "@/utils/log"; import { Ionicons } from "@expo/vector-icons"; -import { useNavigation, useRouter } from "expo-router"; -import { useAtom } from "jotai"; -import React, { useEffect, useMemo, useRef } from "react"; -import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; -import { Button } from "@/components/Button"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; -import { t } from 'i18next'; -import { DownloadSize } from "@/components/downloads/DownloadSize"; import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; +import { useNavigation, useRouter } from "expo-router"; +import { t } from "i18next"; +import { useAtom } from "jotai"; +import React, { useEffect, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; -import { writeToLog } from "@/utils/log"; export default function page() { const navigation = useNavigation(); @@ -45,7 +45,7 @@ export default function page() { const groupedBySeries = useMemo(() => { try { const episodes = downloadedFiles?.filter( - (f) => f.item.Type === "Episode" + (f) => f.item.Type === "Episode", ); const series: { [key: string]: DownloadedItem[] } = {}; episodes?.forEach((e) => { @@ -73,14 +73,22 @@ export default function page() { const deleteMovies = () => deleteFileByType("Movie") - .then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully"))) + .then(() => + toast.success( + t("home.downloads.toasts.deleted_all_movies_successfully"), + ), + ) .catch((reason) => { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_movies")); }); const deleteShows = () => deleteFileByType("Episode") - .then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully"))) + .then(() => + toast.success( + t("home.downloads.toasts.deleted_all_tvseries_successfully"), + ), + ) .catch((reason) => { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); @@ -97,26 +105,28 @@ export default function page() { paddingBottom: 100, }} > - - + + {settings?.downloadMethod === DownloadMethod.Remux && ( - - {t("home.downloads.queue")} - + + + {t("home.downloads.queue")} + + {t("home.downloads.queue_hint")} - + {queue.map((q, index) => ( router.push(`/(auth)/items/page?id=${q.item.Id}`) } - className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" + className='relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between' key={index} > - {q.item.Name} - + {q.item.Name} + {q.item.Type} @@ -129,14 +139,16 @@ export default function page() { }); }} > - + ))} {queue.length === 0 && ( - {t("home.downloads.no_items_in_queue")} + + {t("home.downloads.no_items_in_queue")} + )} )} @@ -145,17 +157,19 @@ export default function page() { {movies.length > 0 && ( - - - {t("home.downloads.movies")} - - {movies?.length} + + + + {t("home.downloads.movies")} + + + {movies?.length} - + {movies?.map((item) => ( - + ))} @@ -164,20 +178,22 @@ export default function page() { )} {groupedBySeries.length > 0 && ( - - - {t("home.downloads.tvseries")} - - + + + + {t("home.downloads.tvseries")} + + + {groupedBySeries?.length} - + {groupedBySeries?.map((items) => ( )} {downloadedFiles?.length === 0 && ( - - {t("home.downloads.no_downloaded_items")} + + + {t("home.downloads.no_downloaded_items")} + )} @@ -215,14 +233,14 @@ export default function page() { )} > - - - - @@ -248,6 +266,6 @@ function migration_20241124() { style: "destructive", onPress: async () => await deleteAllFiles(), }, - ] + ], ); } diff --git a/app/(auth)/(tabs)/(home)/intro/page.tsx b/app/(auth)/(tabs)/(home)/intro/page.tsx index dcbd3bf9..6b914dd8 100644 --- a/app/(auth)/(tabs)/(home)/intro/page.tsx +++ b/app/(auth)/(tabs)/(home)/intro/page.tsx @@ -15,26 +15,26 @@ export default function page() { useFocusEffect( useCallback(() => { storage.set("hasShownIntro", true); - }, []) + }, []), ); return ( - + - + {t("home.intro.welcome_to_streamyfin")} - + {t("home.intro.a_free_and_open_source_client_for_jellyfin")} - + {t("home.intro.features_title")} - {t("home.intro.features_description")} - + {t("home.intro.features_description")} + - - Jellyseerr - + + Jellyseerr + {t("home.intro.jellyseerr_feature_description")} - + - + - - + + {t("home.intro.downloads_feature_title")} - + {t("home.intro.downloads_feature_description")} - + - + - - Chromecast - + + Chromecast + {t("home.intro.chromecast_feature_description")} - + - + - - + + {t("home.intro.centralised_settings_plugin_title")} - + {t("home.intro.centralised_settings_plugin_description")}{" "} { Linking.openURL( - "https://github.com/streamyfin/jellyfin-plugin-streamyfin" + "https://github.com/streamyfin/jellyfin-plugin-streamyfin", ); }} > @@ -120,7 +120,7 @@ export default function page() { onPress={() => { router.back(); }} - className="mt-4" + className='mt-4' > {t("home.intro.done_button")} @@ -129,9 +129,9 @@ export default function page() { router.back(); router.push("/settings"); }} - className="mt-4" + className='mt-4' > - + {t("home.intro.go_to_settings_button")} diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index c797b83e..590d89db 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -1,21 +1,29 @@ +import { Badge } from "@/components/Badge"; +import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; -import { useSessions, useSessionsProps } from "@/hooks/useSessions"; +import Poster from "@/components/posters/Poster"; +import { useInterval } from "@/hooks/useInterval"; +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { formatBitrate } from "@/utils/bitrate"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { formatTimeString } from "@/utils/time"; +import { + AntDesign, + Entypo, + Ionicons, + MaterialCommunityIcons, +} from "@expo/vector-icons"; +import { + HardwareAccelerationType, + type SessionInfoDto, +} from "@jellyfin/sdk/lib/generated-client"; import { FlashList } from "@shopify/flash-list"; +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; -import { Loader } from "@/components/Loader"; -import { HardwareAccelerationType, SessionInfoDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtomValue } from "jotai"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import Poster from "@/components/posters/Poster"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { useInterval } from "@/hooks/useInterval"; -import React, { useEffect, useMemo, useState } from "react"; -import { formatTimeString } from "@/utils/time"; -import { formatBitrate } from "@/utils/bitrate"; -import { Ionicons, Entypo, AntDesign, MaterialCommunityIcons } from "@expo/vector-icons"; -import { Badge } from "@/components/Badge"; -import { useQuery } from "@tanstack/react-query"; export default function page() { const { sessions, isLoading } = useSessions({} as useSessionsProps); @@ -23,21 +31,23 @@ export default function page() { if (isLoading) return ( - + ); if (!sessions || sessions.length == 0) return ( - - {t("home.sessions.no_active_sessions")} + + + {t("home.sessions.no_active_sessions")} + ); return ( { } return Math.round( - (100 / session.NowPlayingItem?.RunTimeTicks) * (session.NowPlayingItem?.RunTimeTicks - remainingTicks) + (100 / session.NowPlayingItem?.RunTimeTicks) * + (session.NowPlayingItem?.RunTimeTicks - remainingTicks), ); }; useEffect(() => { const currentTime = session.PlayState?.PositionTicks; const duration = session.NowPlayingItem?.RunTimeTicks; - if (duration !== null && duration !== undefined && currentTime !== null && currentTime !== undefined) { + if ( + duration !== null && + duration !== undefined && + currentTime !== null && + currentTime !== undefined + ) { const remainingTimeTicks = duration - currentTime; setRemainingTicks(remainingTimeTicks); } @@ -85,9 +101,11 @@ const SessionCard = ({ session }: SessionCardProps) => { const { data: ipInfo } = useQuery({ queryKey: ["ipinfo", session.RemoteEndPoint], - cacheTime: Infinity, + cacheTime: Number.POSITIVE_INFINITY, queryFn: async () => { - const resp = await api.axiosInstance.get(`https://freeipapi.com/api/json/${session.RemoteEndPoint}`); + const resp = await api.axiosInstance.get( + `https://freeipapi.com/api/json/${session.RemoteEndPoint}`, + ); return resp.data; }, }); @@ -95,18 +113,23 @@ const SessionCard = ({ session }: SessionCardProps) => { useInterval(tick, 1000); return ( - - - - + + + + - - - + + + {session.NowPlayingItem?.Type === "Episode" ? ( <> - {session.NowPlayingItem?.Name} - + + {session.NowPlayingItem?.Name} + + {`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`} {" - "} {session.NowPlayingItem.SeriesName} @@ -114,13 +137,19 @@ const SessionCard = ({ session }: SessionCardProps) => { ) : ( <> - {session.NowPlayingItem?.Name} - {session.NowPlayingItem?.ProductionYear} - {session.NowPlayingItem?.SeriesName} + + {session.NowPlayingItem?.Name} + + + {session.NowPlayingItem?.ProductionYear} + + + {session.NowPlayingItem?.SeriesName} + )} - + {session.UserName} {"\n"} {session.Client} @@ -130,21 +159,21 @@ const SessionCard = ({ session }: SessionCardProps) => { {ipInfo?.cityName} {ipInfo?.countryCode} - - - - + + + + {!session.PlayState?.IsPaused ? ( - + ) : ( - + )} - + {formatTimeString(remainingTicks, "tick")} left - + { const iconMap = { - bitrate: , - codec: , - videoRange: , - resolution: , - language: , - audioChannels: , - hwType: , + bitrate: , + codec: , + videoRange: ( + + ), + resolution: , + language: , + audioChannels: , + hwType: , } as const; const icon = (val: string) => { - return iconMap[val as keyof typeof iconMap] ?? ; + return ( + iconMap[val as keyof typeof iconMap] ?? ( + + ) + ); }; const formatVal = (key: string, val: any) => { @@ -195,8 +230,8 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { .map(([key]) => ( @@ -216,7 +251,7 @@ interface StreamProps { interface TranscodingStreamViewProps { title: string | undefined; value?: string; - isTranscoding: Boolean; + isTranscoding: boolean; transcodeValue?: string | undefined | null; properties: StreamProps; transcodeProperties?: StreamProps; @@ -231,20 +266,26 @@ const TranscodingStreamView = ({ transcodeValue, }: TranscodingStreamViewProps) => { return ( - - - {title} - + + + + {title} + + {isTranscoding && transcodeProperties ? ( <> - - - + + + - + @@ -256,21 +297,29 @@ const TranscodingStreamView = ({ const TranscodingView = ({ session }: SessionCardProps) => { const videoStream = useMemo(() => { - return session.NowPlayingItem?.MediaStreams?.filter((s) => s.Type == "Video")[0]; + return session.NowPlayingItem?.MediaStreams?.filter( + (s) => s.Type == "Video", + )[0]; }, [session]); const audioStream = useMemo(() => { const index = session.PlayState?.AudioStreamIndex; - return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; }, [session.PlayState?.AudioStreamIndex]); const subtitleStream = useMemo(() => { const index = session.PlayState?.SubtitleStreamIndex; - return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; }, [session.PlayState?.SubtitleStreamIndex]); const isTranscoding = useMemo(() => { - return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo; + return ( + session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo + ); }, [session.PlayState?.PlayMethod, session.TranscodingInfo]); const videoStreamTitle = () => { @@ -278,9 +327,9 @@ const TranscodingView = ({ session }: SessionCardProps) => { }; return ( - + { bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.VideoCodec, }} - isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} + isTranscoding={ + isTranscoding && !session.TranscodingInfo?.IsVideoDirect + ? true + : false + } /> { codec: session.TranscodingInfo?.AudioCodec, audioChannels: session.TranscodingInfo?.AudioChannels?.toString(), }} - isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} + isTranscoding={ + isTranscoding && !session.TranscodingInfo?.IsVideoDirect + ? true + : false + } /> {subtitleStream && ( <> - + {t("home.settings.log_out_button")} @@ -61,15 +61,15 @@ export default function settings() { paddingRight: insets.right, }} > - + - + - - - + + + @@ -90,7 +90,7 @@ export default function settings() { title={t("home.settings.intro.show_intro")} /> { storage.set("hasShownIntro", false); }} @@ -98,7 +98,7 @@ export default function settings() { /> - + router.push("/settings/logs/page")} @@ -106,7 +106,7 @@ export default function settings() { title={t("home.settings.logs.logs_title")} /> diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index 5b96ddbc..a90f7ef8 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -1,15 +1,15 @@ +import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; -import { Loader } from "@/components/Loader"; +import DisabledSetting from "@/components/settings/DisabledSetting"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; +import { useTranslation } from "react-i18next"; import { Switch, View } from "react-native"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -18,7 +18,7 @@ export default function page() { const { t } = useTranslation(); - const { data, isLoading: isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ["user-views", user?.Id], queryFn: async () => { const response = await getUserViewsApi(api!).getUserViews({ @@ -33,7 +33,7 @@ export default function page() { if (isLoading) return ( - + ); @@ -41,7 +41,7 @@ export default function page() { return ( {data?.map((view) => ( @@ -59,8 +59,8 @@ export default function page() { ))} - - {t("home.settings.other.select_liraries_you_want_to_hide")} + + {t("home.settings.other.select_liraries_you_want_to_hide")} ); diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx index 5da08ff1..507d01e2 100644 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx @@ -1,6 +1,6 @@ +import DisabledSetting from "@/components/settings/DisabledSetting"; import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { useSettings } from "@/utils/atoms/settings"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -8,7 +8,7 @@ export default function page() { return ( diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx index 1c59ba15..32fd3617 100644 --- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx @@ -1,17 +1,17 @@ import { Text } from "@/components/common/Text"; import { useLog } from "@/utils/log"; -import { ScrollView, View } from "react-native"; import { useTranslation } from "react-i18next"; +import { ScrollView, View } from "react-native"; export default function page() { const { logs } = useLog(); const { t } = useTranslation(); return ( - - + + {logs?.map((log, index) => ( - + {log.level} - + {log.message} ))} {logs?.length === 0 && ( - {t("home.settings.logs.no_logs_available")} + + {t("home.settings.logs.no_logs_available")} + )} diff --git a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx index b67f6ea0..c13390da 100644 --- a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx @@ -6,7 +6,8 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; import { useTranslation } from "react-i18next"; -import React, {useEffect, useMemo, useState} from "react"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import React, { useEffect, useMemo, useState } from "react"; import { Linking, Switch, @@ -15,7 +16,6 @@ import { View, } from "react-native"; import { toast } from "sonner-native"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); @@ -39,7 +39,10 @@ export default function page() { }; const disabled = useMemo(() => { - return pluginSettings?.searchEngine?.locked === true && pluginSettings?.marlinServerUrl?.locked === true + return ( + pluginSettings?.searchEngine?.locked === true && + pluginSettings?.marlinServerUrl?.locked === true + ); }, [pluginSettings]); useEffect(() => { @@ -47,7 +50,9 @@ export default function page() { navigation.setOptions({ headerRight: () => ( onSave(value)}> - {t("home.settings.plugins.marlin_search.save_button")} + + {t("home.settings.plugins.marlin_search.save_button")} + ), }); @@ -57,17 +62,16 @@ export default function page() { if (!settings) return null; return ( - + { updateSettings({ searchEngine: "Jellyfin" }); queryClient.invalidateQueries({ queryKey: ["search"] }); @@ -87,28 +91,30 @@ export default function page() { - - {t("home.settings.plugins.marlin_search.url")} + + + {t("home.settings.plugins.marlin_search.url")} + setValue(text)} /> - + {t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "} - + {t("home.settings.plugins.marlin_search.read_more_about_marlin")} diff --git a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx index 8ffc2fc7..1c53d7e2 100644 --- a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx @@ -1,4 +1,5 @@ import { Text } from "@/components/common/Text"; +import DisabledSetting from "@/components/settings/DisabledSetting"; import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; @@ -8,10 +9,9 @@ import { useMutation } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { toast } from "sonner-native"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); @@ -67,8 +67,12 @@ export default function page() { saveMutation.isPending ? ( ) : ( - onSave(optimizedVersionsServerUrl)}> - {t("home.settings.downloads.save_button")} + onSave(optimizedVersionsServerUrl)} + > + + {t("home.settings.downloads.save_button")} + ), }); @@ -78,7 +82,7 @@ export default function page() { return ( { const local = useLocalSearchParams(); @@ -68,7 +68,7 @@ const page: React.FC = () => { return response.data; }, - [api, user?.Id, actorId] + [api, user?.Id, actorId], ); const backdropUrl = useMemo( @@ -79,12 +79,12 @@ const page: React.FC = () => { quality: 90, width: 1000, }), - [item] + [item], ); if (l1) return ( - + ); @@ -105,13 +105,13 @@ const page: React.FC = () => { /> } > - - - + + + - + {t("item_card.appeared_in")} { queryFn={fetchItems} queryKey={["actor", "movies", actorId]} /> - + ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx index 366ca284..b23ae9d7 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx @@ -1,22 +1,23 @@ +import { ItemCardText } from "@/components/ItemCardText"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { FilterButton } from "@/components/filters/FilterButton"; import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; -import { ItemCardText } from "@/components/ItemCardText"; import { ItemPoster } from "@/components/posters/ItemPoster"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { + SortByOption, + SortOrderOption, genreFilterAtom, sortByAtom, - SortByOption, sortOptions, sortOrderAtom, - SortOrderOption, sortOrderOptions, tagsFilterAtom, yearFilterAtom, } from "@/utils/atoms/filters"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, ItemSortBy, @@ -29,11 +30,11 @@ import { import { FlashList } from "@shopify/flash-list"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { FlatList, View } from "react-native"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { FlatList, View } from "react-native"; const page: React.FC = () => { const searchParams = useLocalSearchParams(); @@ -43,7 +44,7 @@ const page: React.FC = () => { const [user] = useAtom(userAtom); const navigation = useNavigation(); const [orientation, setOrientation] = useState( - ScreenOrientation.Orientation.PORTRAIT_UP + ScreenOrientation.Orientation.PORTRAIT_UP, ); const { t } = useTranslation(); @@ -111,7 +112,7 @@ const page: React.FC = () => { recursive: true, genres: selectedGenres, tags: selectedTags, - years: selectedYears.map((year) => parseInt(year)), + years: selectedYears.map((year) => Number.parseInt(year)), includeItemTypes: ["Movie", "Series"], }); @@ -126,7 +127,7 @@ const page: React.FC = () => { selectedTags, sortBy, sortOrder, - ] + ], ); const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -151,7 +152,7 @@ const page: React.FC = () => { const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -188,8 +189,8 @@ const page: React.FC = () => { index % 3 === 0 ? "flex-end" : (index + 1) % 3 === 0 - ? "flex-start" - : "center", + ? "flex-start" + : "center", width: "89%", }} > @@ -199,14 +200,14 @@ const page: React.FC = () => { ), - [orientation] + [orientation], ); const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []); const ListHeaderComponent = useCallback( () => ( - + { key: "genre", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -259,13 +260,13 @@ const page: React.FC = () => { key: "year", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -284,13 +285,13 @@ const page: React.FC = () => { key: "tags", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -311,9 +312,9 @@ const page: React.FC = () => { key: "sortBy", component: ( sortOptions.map((s) => s.key)} set={setSortBy} values={sortBy} @@ -331,9 +332,9 @@ const page: React.FC = () => { key: "sortOrder", component: ( sortOrderOptions.map((s) => s.key)} set={setSortOrder} values={sortOrder} @@ -368,7 +369,7 @@ const page: React.FC = () => { sortOrder, setSortOrder, isFetching, - ] + ], ); if (!collection) return null; @@ -376,8 +377,10 @@ const page: React.FC = () => { return ( - {t("search.no_results")} + + + {t("search.no_results")} + } extraData={[ @@ -387,7 +390,7 @@ const page: React.FC = () => { sortBy, sortOrder, ]} - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' data={flatData} renderItem={renderItem} keyExtractor={keyExtractor} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx index a61114bd..1f9d322b 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx @@ -1,11 +1,13 @@ -import { Text } from "@/components/common/Text"; import { ItemContent } from "@/components/ItemContent"; +import { Text } from "@/components/common/Text"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; -import React, { useEffect } from "react"; +import type React from "react"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import Animated, { runOnJS, @@ -13,7 +15,6 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { useTranslation } from "react-i18next"; const Page: React.FC = () => { const [api] = useAtom(apiAtom); @@ -75,36 +76,36 @@ const Page: React.FC = () => { if (isError) return ( - + {t("item_card.could_not_load_item")} ); return ( - + - - - - - - - + + + + + + + - - - - + + + + {item && } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx index cf8111bb..1f4b139d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx @@ -1,41 +1,43 @@ -import {useLocalSearchParams} from "expo-router"; -import React, {useMemo,} from "react"; -import {useInfiniteQuery} from "@tanstack/react-query"; -import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Image} from "expo-image"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {COMPANY_LOGO_IMAGE_FILTER} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import {uniqBy} from "lodash"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; +import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import { + type MovieResult, + Results, + type TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import { uniqBy } from "lodash"; +import React, { useMemo } from "react"; export default function page() { const local = useLocalSearchParams(); - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); - const {companyId, name, image, type} = local as unknown as { - companyId: string, - name: string, - image: string, - type: DiscoverSliderType + const { companyId, name, image, type } = local as unknown as { + companyId: string; + name: string; + image: string; + type: DiscoverSliderType; }; - const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "company", type, companyId], - queryFn: async ({pageParam}) => { - let params: any = { + queryFn: async ({ pageParam }) => { + const params: any = { page: Number(pageParam), }; return jellyseerrApi?.discover( - ( - type == DiscoverSliderType.NETWORKS - ? Endpoints.DISCOVER_TV_NETWORK - : Endpoints.DISCOVER_MOVIES_STUDIO - ) + `/${companyId}`, - params - ) + (type == DiscoverSliderType.NETWORKS + ? Endpoints.DISCOVER_TV_NETWORK + : Endpoints.DISCOVER_MOVIES_STUDIO) + `/${companyId}`, + params, + ); }, enabled: !!jellyseerrApi && !!companyId, initialPageParam: 1, @@ -46,46 +48,58 @@ export default function page() { }); const flatData = useMemo( - () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [], - [data] + () => + uniqBy( + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results ?? []), + "id", + ) ?? [], + [data], ); const backdrops = useMemo( - () => jellyseerrApi - ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, flatData] + () => + jellyseerrApi + ? flatData.map((r) => + jellyseerrApi.imageProxy( + (r as TvResult | MovieResult).backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, flatData], ); return ( item.id.toString()} onEndReached={() => { if (hasNextPage) { - fetchNextPage() + fetchNextPage(); } }} logo={ } - renderItem={(item, index) => + renderItem={(item, index) => ( - } + )} /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx index dbbce320..0d94a9a5 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx @@ -1,42 +1,46 @@ -import {router, useLocalSearchParams, useSegments,} from "expo-router"; -import React, {useMemo,} from "react"; -import {TouchableOpacity} from "react-native"; -import {useInfiniteQuery} from "@tanstack/react-query"; -import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Text} from "@/components/common/Text"; -import Poster from "@/components/posters/Poster"; +import { Text } from "@/components/common/Text"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {uniqBy} from "lodash"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; +import Poster from "@/components/posters/Poster"; +import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import { + type MovieResult, + Results, + type TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { router, useLocalSearchParams, useSegments } from "expo-router"; +import { uniqBy } from "lodash"; +import React, { useMemo } from "react"; +import { TouchableOpacity } from "react-native"; export default function page() { const local = useLocalSearchParams(); - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); - const {genreId, name, type} = local as unknown as { - genreId: string, - name: string, - type: DiscoverSliderType + const { genreId, name, type } = local as unknown as { + genreId: string; + name: string; + type: DiscoverSliderType; }; - const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "company", type, genreId], - queryFn: async ({pageParam}) => { - let params: any = { + queryFn: async ({ pageParam }) => { + const params: any = { page: Number(pageParam), - genre: genreId + genre: genreId, }; return jellyseerrApi?.discover( - type == DiscoverSliderType.MOVIE_GENRES - ? Endpoints.DISCOVER_MOVIES - : Endpoints.DISCOVER_TV, - params - ) + type == DiscoverSliderType.MOVIE_GENRES + ? Endpoints.DISCOVER_MOVIES + : Endpoints.DISCOVER_TV, + params, + ); }, enabled: !!jellyseerrApi && !!genreId, initialPageParam: 1, @@ -47,41 +51,54 @@ export default function page() { }); const flatData = useMemo( - () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [], - [data] + () => + uniqBy( + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results ?? []), + "id", + ) ?? [], + [data], ); const backdrops = useMemo( - () => jellyseerrApi - ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, flatData] + () => + jellyseerrApi + ? flatData.map((r) => + jellyseerrApi.imageProxy( + (r as TvResult | MovieResult).backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, flatData], ); return ( item.id.toString()} onEndReached={() => { if (hasNextPage) { - fetchNextPage() + fetchNextPage(); } }} logo={ + shadowRadius: 10, + }} + > {name} } - renderItem={(item, index) => + renderItem={(item, index) => ( - } + )} /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index 45318462..845a7676 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -1,27 +1,29 @@ import { Button } from "@/components/Button"; -import { Text } from "@/components/common/Text"; import { GenreTags } from "@/components/GenreTags"; -import Cast from "@/components/jellyseerr/Cast"; -import DetailFacts from "@/components/jellyseerr/DetailFacts"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; import { JellyserrRatings } from "@/components/Ratings"; +import { Text } from "@/components/common/Text"; +import Cast from "@/components/jellyseerr/Cast"; +import DetailFacts from "@/components/jellyseerr/DetailFacts"; import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; import { ItemActions } from "@/components/series/SeriesActions"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import { - IssueType, + type IssueType, IssueTypeName, } from "@/utils/jellyseerr/server/constants/issue"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import { useTranslation } from "react-i18next"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Ionicons } from "@expo/vector-icons"; import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetModal, BottomSheetTextInput, BottomSheetView, @@ -29,20 +31,16 @@ import { import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Platform, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; import RequestModal from "@/components/jellyseerr/RequestModal"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; -import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); @@ -83,8 +81,8 @@ const Page: React.FC = () => { refetchInterval: 0, queryFn: async () => { return mediaType === MediaType.MOVIE - ? jellyseerrApi?.movieDetails(result.id!!) - : jellyseerrApi?.tvDetails(result.id!!); + ? jellyseerrApi?.movieDetails(result.id!) + : jellyseerrApi?.tvDetails(result.id!); }, }); @@ -99,7 +97,7 @@ const Page: React.FC = () => { appearsOnIndex={0} /> ), - [] + [], ); const submitIssue = useCallback(() => { @@ -114,15 +112,18 @@ const Page: React.FC = () => { } }, [jellyseerrApi, details, result, issueType, issueMessage]); - const setRequestBody = useCallback((body: MediaRequestBody) => { - _setRequestBody(body) - advancedReqModalRef?.current?.present?.(); - }, [requestBody, _setRequestBody, advancedReqModalRef]) + const setRequestBody = useCallback( + (body: MediaRequestBody) => { + _setRequestBody(body); + advancedReqModalRef?.current?.present?.(); + }, + [requestBody, _setRequestBody, advancedReqModalRef], + ); const request = useCallback(async () => { const body: MediaRequestBody = { - mediaId: Number(result.id!!), - mediaType: mediaType!!, + mediaId: Number(result.id!), + mediaType: mediaType!, tvdbId: details?.externalIds?.tvdbId, seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0) @@ -130,7 +131,7 @@ const Page: React.FC = () => { }; if (hasAdvancedRequestPermission) { - setRequestBody(body) + setRequestBody(body); return; } @@ -141,14 +142,14 @@ const Page: React.FC = () => { () => (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && mediaType === MediaType.TV, - [details] + [details], ); useEffect(() => { if (details) { navigation.setOptions({ headerRight: () => ( - + ), @@ -158,14 +159,14 @@ const Page: React.FC = () => { return ( @@ -180,7 +181,7 @@ const Page: React.FC = () => { source={{ uri: jellyseerrApi?.imageProxy( result.backdropPath, - "w1920_and_h800_multi_faces" + "w1920_and_h800_multi_faces", ), }} /> @@ -190,12 +191,12 @@ const Page: React.FC = () => { width: "100%", height: "100%", }} - className="flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900" + className='flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900' > @@ -203,23 +204,31 @@ const Page: React.FC = () => { } > - - - - - - + + + + + + {mediaTitle} - {releaseYear} + {releaseYear} { }} /> - + g.name) || []} /> {isLoading || isFetching ? ( - + ) : canRequest ? ( - ) : ( )} - + {mediaType === MediaType.TV && ( @@ -261,13 +270,11 @@ const Page: React.FC = () => { details={details as TvDetails} refetch={refetch} hasAdvancedRequest={hasAdvancedRequestPermission} - onAdvancedRequest={(data) => - setRequestBody(data) - } + onAdvancedRequest={(data) => setRequestBody(data)} /> )} @@ -278,11 +285,11 @@ const Page: React.FC = () => { ref={advancedReqModalRef} requestBody={requestBody} title={mediaTitle} - id={result.id!!} + id={result.id!} type={mediaType} isAnime={isAnime} onRequested={() => { - _setRequestBody(undefined) + _setRequestBody(undefined); advancedReqModalRef?.current?.close(); refetch(); }} @@ -300,22 +307,22 @@ const Page: React.FC = () => { backdropComponent={renderBackdrop} > - + - + {t("jellyseerr.whats_wrong")} - - + + - - + + {t("jellyseerr.issue_type")} - - + + {issueType ? IssueTypeName[issueType] : t("jellyseerr.select_an_issue")} @@ -325,8 +332,8 @@ const Page: React.FC = () => { { - + { /> - diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx index bd0fc216..bbe9f6cc 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx @@ -1,25 +1,30 @@ -import { - useLocalSearchParams, - useSegments, -} from "expo-router"; -import React, { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { Text } from "@/components/common/Text"; -import { Image } from "expo-image"; import { OverviewText } from "@/components/OverviewText"; -import {orderBy, uniqBy} from "lodash"; -import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; +import { Text } from "@/components/common/Text"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useLocalSearchParams, useSegments } from "expo-router"; +import { orderBy, uniqBy } from "lodash"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; export default function page() { const local = useLocalSearchParams(); const { t } = useTranslation(); - const { jellyseerrApi, jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrApi, + jellyseerrUser, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const { personId } = local as { personId: string }; @@ -34,18 +39,27 @@ export default function page() { const castedRoles: PersonCreditCast[] = useMemo( () => - uniqBy(orderBy( - data?.combinedCredits?.cast, - ["voteCount", "voteAverage"], - "desc" - ), 'id'), - [data?.combinedCredits] + uniqBy( + orderBy( + data?.combinedCredits?.cast, + ["voteCount", "voteAverage"], + "desc", + ), + "id", + ), + [data?.combinedCredits], ); const backdrops = useMemo( - () => jellyseerrApi - ? castedRoles.map((c) => jellyseerrApi.imageProxy(c.backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, data?.combinedCredits] + () => + jellyseerrApi + ? castedRoles.map((c) => + jellyseerrApi.imageProxy( + c.backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, data?.combinedCredits], ); return ( @@ -58,15 +72,15 @@ export default function page() { ( <> - - {data?.details?.name} - - + {data?.details?.name} + {t("jellyseerr.born")}{" "} - {new Date(data?.details?.birthday!!).toLocaleDateString( + {new Date(data?.details?.birthday!).toLocaleDateString( `${locale}-${region}`, { year: "numeric", month: "long", day: "numeric", - } + }, )}{" "} | {data?.details?.placeOfBirth} )} MainContent={() => ( - + + )} + renderItem={(item, index) => ( + )} - renderItem={(item, index) => } /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx index 7225e677..9c6625e6 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx @@ -3,7 +3,10 @@ import type { MaterialTopTabNavigationOptions, } from "@react-navigation/material-top-tabs"; import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; -import { ParamListBase, TabNavigationState } from "@react-navigation/native"; +import type { + ParamListBase, + TabNavigationState, +} from "@react-navigation/native"; import { Stack, withLayoutContext } from "expo-router"; import React from "react"; @@ -21,8 +24,8 @@ const Layout = () => { <> { tabBarScrollEnabled: true, }} > - - - - + + + + ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx index dd1c1f85..eae563fb 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx @@ -31,13 +31,13 @@ export default function page() { }); return ( - + ( - - + + - {item.Name} + {item.Name} )} /> diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx index 398d74b6..7dbe3461 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx @@ -9,6 +9,7 @@ import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button, Dimensions, @@ -17,7 +18,6 @@ import { View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; const HOUR_HEIGHT = 30; const ITEMS_PER_PAGE = 20; @@ -71,7 +71,7 @@ export default function page() { MaxStartDate: endOfDay.toISOString(), MinEndDate: isToday ? now.toISOString() : startOfDay.toISOString(), ChannelIds: channels?.Items?.map((c) => c.Id).filter( - Boolean + Boolean, ) as string[], ImageTypeLimit: 1, EnableImages: false, @@ -100,7 +100,7 @@ export default function page() { return ( - - + + {channels?.Items?.map((c, i) => ( - + - + {channels?.Items?.map((c, i) => ( = ({ }) => { const { t } = useTranslation(); return ( - + @@ -199,11 +199,11 @@ const PageButtons: React.FC = ({ {t("live_tv.previous")} - Page {currentPage} + Page {currentPage} = ({ {t("live_tv.next")} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx index 76c07643..ec0a78ba 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx @@ -1,13 +1,13 @@ import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; import { TAB_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtom } from "jotai"; import React from "react"; +import { useTranslation } from "react-i18next"; import { ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; export default function page() { const [api] = useAtom(apiAtom); @@ -19,7 +19,7 @@ export default function page() { return ( - + diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx index 4068f8a3..8fef80bd 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx @@ -1,12 +1,12 @@ import { Text } from "@/components/common/Text"; import React from "react"; -import { View } from "react-native"; import { useTranslation } from "react-i18next"; +import { View } from "react-native"; export default function page() { const { t } = useTranslation(); return ( - + {t("live_tv.coming_soon")} ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx index eb9b660d..35845572 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -14,9 +14,10 @@ import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; -import React, { useEffect, useMemo } from "react"; -import { Platform, View } from "react-native"; +import type React from "react"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { Platform, View } from "react-native"; const page: React.FC = () => { const navigation = useNavigation(); @@ -49,7 +50,7 @@ const page: React.FC = () => { quality: 90, width: 1000, }), - [item] + [item], ); const logoUrl = useMemo( @@ -58,7 +59,7 @@ const page: React.FC = () => { api, item, }), - [item] + [item], ); const { data: allEpisodes, isLoading } = useQuery({ @@ -83,22 +84,22 @@ const page: React.FC = () => { item && allEpisodes && allEpisodes.length > 0 && ( - + {!Platform.isTV && ( <> ( - + )} DownloadedIconComponent={() => ( )} /> @@ -142,9 +143,9 @@ const page: React.FC = () => { } > - + - + diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index a7b9cc1f..ec0e7250 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -1,35 +1,35 @@ +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo } from "react"; -import { FlatList, useWindowDimensions, View } from "react-native"; +import { FlatList, View, useWindowDimensions } from "react-native"; +import { ItemCardText } from "@/components/ItemCardText"; +import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { FilterButton } from "@/components/filters/FilterButton"; import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; -import { ItemCardText } from "@/components/ItemCardText"; -import { Loader } from "@/components/Loader"; import { ItemPoster } from "@/components/posters/ItemPoster"; import { useOrientation } from "@/hooks/useOrientation"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { + SortByOption, + SortOrderOption, genreFilterAtom, getSortByPreference, getSortOrderPreference, sortByAtom, - SortByOption, sortByPreferenceAtom, sortOptions, sortOrderAtom, - SortOrderOption, sortOrderOptions, sortOrderPreferenceAtom, tagsFilterAtom, yearFilterAtom, } from "@/utils/atoms/filters"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, BaseItemKind, @@ -40,8 +40,8 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { FlashList } from "@shopify/flash-list"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; const Page = () => { const searchParams = useLocalSearchParams(); @@ -58,7 +58,7 @@ const Page = () => { const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom); const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom); const [sortOrderPreference, setOderByPreference] = useAtom( - sortOrderPreferenceAtom + sortOrderPreferenceAtom, ); const { orientation } = useOrientation(); @@ -88,7 +88,7 @@ const Page = () => { } _setSortBy(sortBy); }, - [libraryId, sortByPreference] + [libraryId, sortByPreference], ); const setSortOrder = useCallback( @@ -102,7 +102,7 @@ const Page = () => { } _setSortOrder(sortOrder); }, - [libraryId, sortOrderPreference] + [libraryId, sortOrderPreference], ); const nrOfCols = useMemo(() => { @@ -169,7 +169,7 @@ const Page = () => { fields: ["PrimaryImageAspectRatio", "SortName"], genres: selectedGenres, tags: selectedTags, - years: selectedYears.map((year) => parseInt(year)), + years: selectedYears.map((year) => Number.parseInt(year)), includeItemTypes: itemType ? [itemType] : undefined, }); @@ -185,7 +185,7 @@ const Page = () => { selectedTags, sortBy, sortOrder, - ] + ], ); const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = @@ -211,7 +211,7 @@ const Page = () => { const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -248,8 +248,8 @@ const Page = () => { ? index % nrOfCols === 0 ? "flex-end" : (index + 1) % nrOfCols === 0 - ? "flex-start" - : "center" + ? "flex-start" + : "center" : "center", width: "89%", }} @@ -260,14 +260,14 @@ const Page = () => { ), - [orientation] + [orientation], ); const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []); const ListHeaderComponent = useCallback( () => ( - + { key: "genre", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -313,13 +313,13 @@ const Page = () => { key: "year", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -338,13 +338,13 @@ const Page = () => { key: "tags", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -365,9 +365,9 @@ const Page = () => { key: "sortBy", component: ( sortOptions.map((s) => s.key)} set={setSortBy} values={sortBy} @@ -385,9 +385,9 @@ const Page = () => { key: "sortOrder", component: ( sortOrderOptions.map((s) => s.key)} set={setSortOrder} values={sortOrder} @@ -422,22 +422,24 @@ const Page = () => { sortOrder, setSortOrder, isFetching, - ] + ], ); const insets = useSafeAreaInsets(); if (isLoading || isLibraryLoading) return ( - + ); if (flatData.length === 0) return ( - - {t("library.no_items_found")} + + + {t("library.no_items_found")} + ); @@ -445,11 +447,13 @@ const Page = () => { - {t("library.no_results")} + + + {t("library.no_results")} + } - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' data={flatData} renderItem={renderItem} extraData={[orientation, nrOfCols]} diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 3406bb09..36647450 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -16,7 +16,7 @@ export default function IndexLayout() { return ( {t("library.options.display")} - + - + {t("library.options.display")} updateSettings({ @@ -75,12 +75,12 @@ export default function IndexLayout() { } > - + {t("library.options.row")} updateSettings({ @@ -92,14 +92,14 @@ export default function IndexLayout() { } > - + {t("library.options.list")} - + {t("library.options.image_style")} - + {t("library.options.poster")} updateSettings({ @@ -141,17 +141,17 @@ export default function IndexLayout() { } > - + {t("library.options.cover")} - + { if (settings.libraryOptions.imageStyle === "poster") @@ -165,12 +165,12 @@ export default function IndexLayout() { }} > - + {t("library.options.show_titles")} { updateSettings({ @@ -182,7 +182,7 @@ export default function IndexLayout() { }} > - + {t("library.options.show_stats")} @@ -195,7 +195,7 @@ export default function IndexLayout() { }} /> ))} { const response = await getUserViewsApi(api!).getUserViews({ @@ -41,7 +41,7 @@ export default function index() { ?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)) .filter((l) => l.CollectionType !== "music") .filter((l) => l.CollectionType !== "books") || [], - [data, settings?.hiddenLibraries] + [data, settings?.hiddenLibraries], ); useEffect(() => { @@ -65,22 +65,24 @@ export default function index() { if (isLoading) return ( - + ); if (!libraries) return ( - - {t("library.no_libraries_found")} + + + {t("library.no_libraries_found")} + ); return ( ) : ( - + ) } estimatedItemSize={200} diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx index ce787316..6156546b 100644 --- a/app/(auth)/(tabs)/(search)/_layout.tsx +++ b/app/(auth)/(tabs)/(search)/_layout.tsx @@ -3,15 +3,15 @@ import { nestedTabPageScreenOptions, } from "@/components/stacks/NestedTabPageStack"; import { Stack } from "expo-router"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function SearchLayout() { const { t } = useTranslation(); return ( ))} - + diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 4d4686b9..aaef84d6 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,30 +1,43 @@ -import {Text} from "@/components/common/Text"; -import {TouchableItemRouter} from "@/components/common/TouchableItemRouter"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import {Tag} from "@/components/GenreTags"; -import {ItemCardText} from "@/components/ItemCardText"; -import {JellyseerrSearchSort, JellyserrIndexPage} from "@/components/jellyseerr/JellyseerrIndexPage"; +import { Tag } from "@/components/GenreTags"; +import { ItemCardText } from "@/components/ItemCardText"; +import { Text } from "@/components/common/Text"; +import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import { FilterButton } from "@/components/filters/FilterButton"; +import { + JellyseerrSearchSort, + JellyserrIndexPage, +} from "@/components/jellyseerr/JellyseerrIndexPage"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; -import {LoadingSkeleton} from "@/components/search/LoadingSkeleton"; -import {SearchItemWrapper} from "@/components/search/SearchItemWrapper"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {apiAtom, userAtom} from "@/providers/JellyfinProvider"; -import {useSettings} from "@/utils/atoms/settings"; -import {BaseItemDto, BaseItemKind,} from "@jellyfin/sdk/lib/generated-client/models"; -import {getItemsApi, getSearchApi} from "@jellyfin/sdk/lib/utils/api"; -import {useQuery} from "@tanstack/react-query"; +import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; +import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { sortOrderOptions } from "@/utils/atoms/filters"; +import { useSettings } from "@/utils/atoms/settings"; +import { eventBus } from "@/utils/eventBus"; +import type { + BaseItemDto, + BaseItemKind, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import {router, useLocalSearchParams, useNavigation} from "expo-router"; -import {useAtom} from "jotai"; -import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,} from "react"; -import {Platform, ScrollView, TouchableOpacity, View} from "react-native"; -import {useSafeAreaInsets} from "react-native-safe-area-context"; -import {useDebounce} from "use-debounce"; -import {useTranslation} from "react-i18next"; -import {eventBus} from "@/utils/eventBus"; -import {sortOrderOptions} from "@/utils/atoms/filters"; -import {FilterButton} from "@/components/filters/FilterButton"; +import { router, useLocalSearchParams, useNavigation } from "expo-router"; +import { useAtom } from "jotai"; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useDebounce } from "use-debounce"; type SearchType = "Library" | "Discover"; @@ -55,8 +68,15 @@ export default function search() { const [settings] = useSettings(); const { jellyseerrApi } = useJellyseerr(); - const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort[JellyseerrSearchSort.DEFAULT] as unknown as JellyseerrSearchSort) - const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc") + const [jellyseerrOrderBy, setJellyseerrOrderBy] = + useState( + JellyseerrSearchSort[ + JellyseerrSearchSort.DEFAULT + ] as unknown as JellyseerrSearchSort, + ); + const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState< + "asc" | "desc" + >("desc"); const searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -112,7 +132,7 @@ export default function search() { return []; // Ensure an empty array is returned in case of an error } }, - [api, searchEngine, settings] + [api, searchEngine, settings], ); type HeaderSearchBarRef = { @@ -220,26 +240,29 @@ export default function search() { return ( <> {jellyseerrApi && ( <> - + setSearchType("Library")}> setSearchType("Discover")}> - {searchType === "Discover" && !loading && noResults && debouncedSearch.length > 0 && ( - - Object.keys(JellyseerrSearchSort).filter(v => isNaN(Number(v)))} - set={value => setJellyseerrOrderBy(value[0])} - values={[jellyseerrOrderBy]} - title={t("library.filters.sort_by")} - renderItemLabel={(item) => t(`home.settings.plugins.jellyseerr.order_by.${item}`)} - showSearch={false} - /> - ["asc", "desc"]} - set={value => setJellyseerrSortOrder(value[0])} - values={[jellyseerrSortOrder]} - title={t("library.filters.sort_order")} - renderItemLabel={(item) => t(`library.filters.${item}`)} - showSearch={false} - /> - - )} + {searchType === "Discover" && + !loading && + noResults && + debouncedSearch.length > 0 && ( + + + Object.keys(JellyseerrSearchSort).filter((v) => + isNaN(Number(v)), + ) + } + set={(value) => setJellyseerrOrderBy(value[0])} + values={[jellyseerrOrderBy]} + title={t("library.filters.sort_by")} + renderItemLabel={(item) => + t(`home.settings.plugins.jellyseerr.order_by.${item}`) + } + showSearch={false} + /> + ["asc", "desc"]} + set={(value) => setJellyseerrSortOrder(value[0])} + values={[jellyseerrSortOrder]} + title={t("library.filters.sort_order")} + renderItemLabel={(item) => t(`library.filters.${item}`)} + showSearch={false} + /> + + )} )} - + @@ -294,14 +326,14 @@ export default function search() { renderItem={(item: BaseItemDto) => ( - + {item.Name} - + {item.ProductionYear} @@ -314,13 +346,13 @@ export default function search() { - + {item.Name} - + {item.ProductionYear} @@ -333,7 +365,7 @@ export default function search() { @@ -347,10 +379,10 @@ export default function search() { - + {item.Name} @@ -363,7 +395,7 @@ export default function search() { @@ -383,22 +415,22 @@ export default function search() { <> {!loading && noResults && debouncedSearch.length > 0 ? ( - + {t("search.no_results_found_for")} - + "{debouncedSearch}" ) : debouncedSearch.length === 0 ? ( - + {exampleSearches.map((e) => ( setSearch(e)} key={e} - className="mb-2" + className='mb-2' > - {e} + {e} ))} diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 6f581ae0..f26309b7 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -1,26 +1,26 @@ import React, { useCallback, useRef } from "react"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { useFocusEffect, useRouter, withLayoutContext } from "expo-router"; import { + type NativeBottomTabNavigationEventMap, createNativeBottomTabNavigator, - NativeBottomTabNavigationEventMap, } from "@bottom-tabs/react-navigation"; const { Navigator } = createNativeBottomTabNavigator(); -import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; +import type { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; import { Colors } from "@/constants/Colors"; import { useSettings } from "@/utils/atoms/settings"; +import { eventBus } from "@/utils/eventBus"; import { storage } from "@/utils/mmkv"; import type { ParamListBase, TabNavigationState, } from "@react-navigation/native"; import { SystemBars } from "react-native-edge-to-edge"; -import { eventBus } from "@/utils/eventBus"; export const NativeTabs = withLayoutContext< BottomTabNavigationOptions, @@ -46,12 +46,12 @@ export default function TabLayout() { clearTimeout(timer); }; } - }, []) + }, []), ); return ( <> - @@ -107,7 +108,7 @@ const Login: React.FC = () => { } else { Alert.alert( t("login.connection_failed"), - t("login.an_unexpected_error_occured") + t("login.an_unexpected_error_occured"), ); } } finally { @@ -176,7 +177,7 @@ const Login: React.FC = () => { if (result === undefined) { Alert.alert( t("login.connection_failed"), - t("login.could_not_connect_to_server") + t("login.could_not_connect_to_server"), ); return; } @@ -195,13 +196,13 @@ const Login: React.FC = () => { { text: t("login.got_it"), }, - ] + ], ); } } catch (error) { Alert.alert( t("login.error_title"), - t("login.failed_to_initiate_quick_connect") + t("login.failed_to_initiate_quick_connect"), ); } }; @@ -213,22 +214,22 @@ const Login: React.FC = () => { > {api?.basePath ? ( <> - - - - + + + + <> {serverName ? ( <> {t("login.login_to_title") + " "} - {serverName} + {serverName} ) : ( t("login.login_title") )} - + {api.basePath} { setCredentials({ ...credentials, username: text }) } value={credentials.username} - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' // Changed from username to oneTimeCode because it is a known issue in RN // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 - textContentType="oneTimeCode" - clearButtonMode="while-editing" + textContentType='oneTimeCode' + clearButtonMode='while-editing' maxLength={500} /> @@ -254,42 +255,42 @@ const Login: React.FC = () => { } value={credentials.password} secureTextEntry - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" - textContentType="password" - clearButtonMode="while-editing" + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + textContentType='password' + clearButtonMode='while-editing' maxLength={500} /> - + - + ) : ( <> - - + + { }} source={require("@/assets/images/StreamyFinFinal.png")} /> - Streamyfin - + Streamyfin + {t("server.enter_url_to_jellyfin_server")} diff --git a/augmentations/api.ts b/augmentations/api.ts index fa20552d..b79e341a 100644 --- a/augmentations/api.ts +++ b/augmentations/api.ts @@ -1,21 +1,21 @@ -import { Api, AUTHORIZATION_HEADER } from "@jellyfin/sdk"; -import { AxiosRequestConfig, AxiosResponse } from "axios"; -import { StreamyfinPluginConfig } from "@/utils/atoms/settings"; +import type { StreamyfinPluginConfig } from "@/utils/atoms/settings"; +import { AUTHORIZATION_HEADER, Api } from "@jellyfin/sdk"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; declare module "@jellyfin/sdk" { interface Api { get( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; post( url: string, data: D, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; delete( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; getStreamyfinPluginConfig(): Promise>; } @@ -23,7 +23,7 @@ declare module "@jellyfin/sdk" { Api.prototype.get = function ( url: string, - config: AxiosRequestConfig = {} + config: AxiosRequestConfig = {}, ): Promise> { return this.axiosInstance.get(`${this.basePath}${url}`, { ...(config ?? {}), @@ -34,7 +34,7 @@ Api.prototype.get = function ( Api.prototype.post = function ( url: string, data: D, - config: AxiosRequestConfig + config: AxiosRequestConfig, ): Promise> { return this.axiosInstance.post(`${this.basePath}${url}`, data, { ...(config || {}), @@ -44,7 +44,7 @@ Api.prototype.post = function ( Api.prototype.delete = function ( url: string, - config: AxiosRequestConfig + config: AxiosRequestConfig, ): Promise> { return this.axiosInstance.delete(`${this.basePath}${url}`, { ...(config || {}), diff --git a/augmentations/mmkv.ts b/augmentations/mmkv.ts index 5667502f..a6b35d22 100644 --- a/augmentations/mmkv.ts +++ b/augmentations/mmkv.ts @@ -1,22 +1,21 @@ -import {MMKV} from "react-native-mmkv"; +import { MMKV } from "react-native-mmkv"; declare module "react-native-mmkv" { interface MMKV { - get(key: string): T | undefined - setAny(key: string, value: any | undefined): void + get(key: string): T | undefined; + setAny(key: string, value: any | undefined): void; } } -MMKV.prototype.get = function (key: string): T | undefined { +MMKV.prototype.get = function (key: string): T | undefined { const serializedItem = this.getString(key); return serializedItem ? JSON.parse(serializedItem) : undefined; -} +}; MMKV.prototype.setAny = function (key: string, value: any | undefined): void { if (value === undefined) { - this.delete(key) - } - else { + this.delete(key); + } else { this.set(key, JSON.stringify(value)); } -} \ No newline at end of file +}; diff --git a/augmentations/number.ts b/augmentations/number.ts index 15b70507..a60f8811 100644 --- a/augmentations/number.ts +++ b/augmentations/number.ts @@ -7,17 +7,19 @@ declare global { } } -Number.prototype.bytesToReadable = function (decimals: number = 2) { +Number.prototype.bytesToReadable = function (decimals = 2) { const bytes = this.valueOf(); - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + return ( + Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i] + ); }; Number.prototype.secondsToMilliseconds = function () { diff --git a/augmentations/string.ts b/augmentations/string.ts index 75a97f05..f4a50b55 100644 --- a/augmentations/string.ts +++ b/augmentations/string.ts @@ -5,12 +5,10 @@ declare global { } String.prototype.toTitle = function () { - return this - .replaceAll("_", " ") - .replace( - /\w\S*/g, - text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase() - ); -} + return this.replaceAll("_", " ").replace( + /\w\S*/g, + (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), + ); +}; -export {}; \ No newline at end of file +export {}; diff --git a/babel.config.js b/babel.config.js index 98ac332d..41dc7e41 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,4 @@ -module.exports = function (api) { +module.exports = (api) => { api.cache(true); return { presets: ["babel-preset-expo"], diff --git a/biome.json b/biome.json index cfee4e6d..fea15f82 100644 --- a/biome.json +++ b/biome.json @@ -7,6 +7,10 @@ "linter": { "enabled": true, "rules": { + "style": { + "useImportType": "off", + "noNonNullAssertion": "off" + }, "recommended": true, "style": { "useImportType": "off", diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx index b190dced..16e15694 100644 --- a/components/AddToFavorites.tsx +++ b/components/AddToFavorites.tsx @@ -1,21 +1,20 @@ - -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useFavorite } from "@/hooks/useFavorite"; -import {View, ViewProps} from "react-native"; import { RoundButton } from "@/components/RoundButton"; -import {FC} from "react"; +import { useFavorite } from "@/hooks/useFavorite"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { FC } from "react"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps { item: BaseItemDto; } -export const AddToFavorites:FC = ({ item, ...props }) => { +export const AddToFavorites: FC = ({ item, ...props }) => { const { isFavorite, toggleFavorite } = useFavorite(item); return ( { source?: MediaSourceInfo; @@ -20,31 +20,31 @@ export const AudioTrackSelector: React.FC = ({ if (Platform.isTV) return null; const audioStreams = useMemo( () => source?.MediaStreams?.filter((x) => x.Type === "Audio"), - [source] + [source], ); const selectedAudioSteam = useMemo( () => audioStreams?.find((x) => x.Index === selected), - [audioStreams, selected] + [audioStreams, selected], ); const { t } = useTranslation(); return ( - - + + {t("item_card.audio")} - - + + {selectedAudioSteam?.DisplayTitle} @@ -52,8 +52,8 @@ export const AudioTrackSelector: React.FC = ({ = ({ ${variant === "gray" && "bg-neutral-800"} `} > - {iconLeft && {iconLeft}} + {iconLeft && {iconLeft}} (b.value || Infinity) - (a.value || Infinity)); +].sort( + (a, b) => + (b.value || Number.POSITIVE_INFINITY) - + (a.value || Number.POSITIVE_INFINITY), +); interface Props extends React.ComponentProps { onChange: (value: Bitrate) => void; @@ -58,10 +62,14 @@ export const BitrateSelector: React.FC = ({ const sorted = useMemo(() => { if (inverted) return BITRATES.sort( - (a, b) => (a.value || Infinity) - (b.value || Infinity) + (a, b) => + (a.value || Number.POSITIVE_INFINITY) - + (b.value || Number.POSITIVE_INFINITY), ); return BITRATES.sort( - (a, b) => (b.value || Infinity) - (a.value || Infinity) + (a, b) => + (b.value || Number.POSITIVE_INFINITY) - + (a.value || Number.POSITIVE_INFINITY), ); }, []); @@ -69,7 +77,7 @@ export const BitrateSelector: React.FC = ({ return ( = ({ > - - + + {t("item_card.quality")} - - + + {BITRATES.find((b) => b.value === selected?.value)?.key} @@ -90,8 +98,8 @@ export const BitrateSelector: React.FC = ({ > = ({ {...props} > {loading ? ( - + ) : ( @@ -72,7 +73,7 @@ export const Button: React.FC> = ({ flex flex-row items-center justify-between w-full ${justify === "between" ? "justify-between" : "justify-center"}`} > - {iconLeft ? iconLeft : } + {iconLeft ? iconLeft : } > = ({ > {children} - {iconRight ? iconRight : } + {iconRight ? iconRight : } )} diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx index 4c66c953..22fd6384 100644 --- a/components/Chromecast.tsx +++ b/components/Chromecast.tsx @@ -1,6 +1,6 @@ import { Feather } from "@expo/vector-icons"; import React, { useCallback, useEffect } from "react"; -import { Platform, TouchableOpacity, ViewProps } from "react-native"; +import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import GoogleCast, { CastButton, CastContext, @@ -45,18 +45,18 @@ export function Chromecast({ const AndroidCastButton = useCallback( () => Platform.OS === "android" ? ( - + ) : ( <> ), - [Platform.OS] + [Platform.OS], ); if (background === "transparent") return ( { if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); @@ -65,13 +65,13 @@ export function Chromecast({ {...props} > - + ); return ( { if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); else CastContext.showCastDialog(); @@ -79,7 +79,7 @@ export function Chromecast({ {...props} > - + ); } diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index a011d23e..02218cff 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -1,12 +1,12 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtomValue } from "jotai"; import { useMemo } from "react"; +import type React from "react"; import { View } from "react-native"; import { WatchedIndicator } from "./WatchedIndicator"; -import React from "react"; -import { Ionicons } from "@expo/vector-icons"; type ContinueWatchingPosterProps = { item: BaseItemDto; @@ -71,7 +71,7 @@ const ContinueWatchingPoster: React.FC = ({ if (!url) return ( - + ); return ( @@ -81,7 +81,7 @@ const ContinueWatchingPoster: React.FC = ({ ${size === "small" ? "w-32" : "w-44"} `} > - + = ({ uri: url, }} cachePolicy={"memory-disk"} - contentFit="cover" - className="w-full h-full" + contentFit='cover' + className='w-full h-full' /> {showPlayButton && ( - - + + )} diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index e7286023..1ea49626 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -10,29 +10,30 @@ import download from "@/utils/profiles/download"; import Ionicons from "@expo/vector-icons/Ionicons"; import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; -import { Href, router, useFocusEffect } from "expo-router"; +import { type Href, router, useFocusEffect } from "expo-router"; +import { t } from "i18next"; import { useAtom } from "jotai"; -import React, { useCallback, useMemo, useRef, useState } from "react"; -import { Alert, Platform, View, ViewProps } from "react-native"; +import type React from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { Alert, Platform, View, type ViewProps } from "react-native"; import { toast } from "sonner-native"; import { AudioTrackSelector } from "./AudioTrackSelector"; -import { Bitrate, BitrateSelector } from "./BitrateSelector"; +import { type Bitrate, BitrateSelector } from "./BitrateSelector"; import { Button } from "./Button"; -import { Text } from "./common/Text"; import { Loader } from "./Loader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import ProgressCircle from "./ProgressCircle"; import { RoundButton } from "./RoundButton"; import { SubtitleTrackSelector } from "./SubtitleTrackSelector"; -import { t } from "i18next"; +import { Text } from "./common/Text"; interface DownloadProps extends ViewProps { items: BaseItemDto[]; @@ -70,16 +71,16 @@ export const DownloadItems: React.FC = ({ settings?.defaultBitrate ?? { key: "Max", value: undefined, - } + }, ); const userCanDownload = useMemo( () => user?.Policy?.EnableContentDownloading, - [user] + [user], ); const usingOptimizedServer = useMemo( () => settings?.downloadMethod === DownloadMethod.Optimized, - [settings] + [settings], ); const bottomSheetModalRef = useRef(null); @@ -99,7 +100,7 @@ export const DownloadItems: React.FC = ({ const itemsNotDownloaded = useMemo( () => items.filter((i) => !downloadedFiles?.some((f) => f.item.Id === i.Id)), - [items, downloadedFiles] + [items, downloadedFiles], ); const allItemsDownloaded = useMemo(() => { @@ -108,7 +109,7 @@ export const DownloadItems: React.FC = ({ }, [items, itemsNotDownloaded]); const itemsProcesses = useMemo( () => processes?.filter((p) => itemIds.includes(p.item.Id)), - [processes, itemIds] + [processes, itemIds], ); const progress = useMemo(() => { @@ -140,7 +141,7 @@ export const DownloadItems: React.FC = ({ params: { episodeSeasonIndex: firstItem.ParentIndexNumber, }, - } as Href) + } as Href), ); }; @@ -160,12 +161,12 @@ export const DownloadItems: React.FC = ({ id: item.Id!, execute: async () => await initiateDownload(item), item, - })) + })), ); } } else { toast.error( - t("home.downloads.toasts.you_are_not_allowed_to_download_files") + t("home.downloads.toasts.you_are_not_allowed_to_download_files"), ); } }, [ @@ -189,7 +190,7 @@ export const DownloadItems: React.FC = ({ (itemsNotDownloaded.length === 1 && !selectedMediaSource?.Id) ) { throw new Error( - "DownloadItem ~ initiateDownload: No api or user or item" + "DownloadItem ~ initiateDownload: No api or user or item", ); } let mediaSource = selectedMediaSource; @@ -220,7 +221,7 @@ export const DownloadItems: React.FC = ({ if (!res) { Alert.alert( t("home.downloads.something_went_wrong"), - t("home.downloads.could_not_get_stream_url_from_jellyfin") + t("home.downloads.could_not_get_stream_url_from_jellyfin"), ); continue; } @@ -250,7 +251,7 @@ export const DownloadItems: React.FC = ({ usingOptimizedServer, startBackgroundDownload, startRemuxing, - ] + ], ); const renderBackdrop = useCallback( @@ -261,7 +262,7 @@ export const DownloadItems: React.FC = ({ appearsOnIndex={0} /> ), - [] + [], ); useFocusEffect( useCallback(() => { @@ -274,7 +275,7 @@ export const DownloadItems: React.FC = ({ setSelectedAudioStream(audioIndex ?? 0); setSelectedSubtitleStream(subtitleIndex ?? -1); setMaxBitrate(bitrate); - }, [items, itemsNotDownloaded, settings]) + }, [items, itemsNotDownloaded, settings]), ); const renderButtonContent = () => { @@ -282,18 +283,18 @@ export const DownloadItems: React.FC = ({ return progress === 0 ? ( ) : ( - + ); } else if (itemsQueued) { - return ; + return ; } else if (allItemsDownloaded) { return ; } else { @@ -331,19 +332,19 @@ export const DownloadItems: React.FC = ({ backdropComponent={renderBackdrop} > - + - + {title} - + {subtitle || t("item_card.download.download_x_item", { item_count: itemsNotDownloaded.length, })} - + = ({ selected={selectedMediaSource} /> {selectedMediaSource && ( - + = ({ )} - - + + {usingOptimizedServer ? t("item_card.download.using_optimized_server") : t("item_card.download.using_default_method")} @@ -411,10 +412,10 @@ export const DownloadSingleItem: React.FC<{ subtitle={item.Name!} items={[item]} MissingDownloadIconComponent={() => ( - + )} DownloadedIconComponent={() => ( - + )} /> ); diff --git a/components/GenreTags.tsx b/components/GenreTags.tsx index ce907d1b..6708269d 100644 --- a/components/GenreTags.tsx +++ b/components/GenreTags.tsx @@ -1,49 +1,57 @@ // GenreTags.tsx -import React from "react"; -import {StyleProp, TextStyle, View, ViewProps} from "react-native"; +import type React from "react"; +import { + type StyleProp, + type TextStyle, + View, + type ViewProps, +} from "react-native"; import { Text } from "./common/Text"; interface TagProps { tags?: string[]; - textClass?: ViewProps["className"] + textClass?: ViewProps["className"]; } -export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], textStyle?: StyleProp} & ViewProps> = ({ - text, - textClass, - textStyle, - ...props -}) => { +export const Tag: React.FC< + { + text: string; + textClass?: ViewProps["className"]; + textStyle?: StyleProp; + } & ViewProps +> = ({ text, textClass, textStyle, ...props }) => { return ( - - {text} + + + {text} + ); }; -export const Tags: React.FC = ({ - tags, - textClass = "text-xs", - tagProps, - ...props -}) => { +export const Tags: React.FC< + TagProps & { tagProps?: ViewProps } & ViewProps +> = ({ tags, textClass = "text-xs", tagProps, ...props }) => { if (!tags || tags.length === 0) return null; return ( - + {tags.map((tag, idx) => ( - + ))} ); }; -export const GenreTags: React.FC<{ genres?: string[]}> = ({ genres }) => { +export const GenreTags: React.FC<{ genres?: string[] }> = ({ genres }) => { return ( - - + + ); }; diff --git a/components/ItemCardText.tsx b/components/ItemCardText.tsx index 2a9995e1..8ce52da2 100644 --- a/components/ItemCardText.tsx +++ b/components/ItemCardText.tsx @@ -1,8 +1,8 @@ -import React from "react"; +import { tc } from "@/utils/textTools"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; import { View } from "react-native"; import { Text } from "./common/Text"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { tc } from "@/utils/textTools"; type ItemCardProps = { item: BaseItemDto; @@ -10,13 +10,13 @@ type ItemCardProps = { export const ItemCardText: React.FC = ({ item }) => { return ( - + {item.Type === "Episode" ? ( <> - + {item.Name} - + {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`} {" - "} {item.SeriesName} @@ -24,8 +24,10 @@ export const ItemCardText: React.FC = ({ item }) => { ) : ( <> - {item.Name} - {item.ProductionYear} + + {item.Name} + + {item.ProductionYear} )} diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 1f9077f8..f27e0f41 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -1,5 +1,5 @@ import { AudioTrackSelector } from "@/components/AudioTrackSelector"; -import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; +import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector"; import { DownloadSingleItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; @@ -19,7 +19,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -86,18 +86,18 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( navigation.setOptions({ headerRight: () => item && ( - + {item.Type !== "Program" && ( - + {!Platform.isTV && ( - + )} - + )} @@ -123,7 +123,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( return ( = React.memo( } > - - - + + + {item.Type !== "Program" && !Platform.isTV && ( - + setSelectedOptions( - (prev) => prev && { ...prev, bitrate: val } + (prev) => prev && { ...prev, bitrate: val }, ) } selected={selectedOptions.bitrate} /> setSelectedOptions( @@ -188,13 +188,13 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, mediaSource: val, - } + }, ) } selected={selectedOptions.mediaSource} /> { setSelectedOptions( @@ -202,7 +202,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, audioIndex: val, - } + }, ); }} selected={selectedOptions.audioIndex} @@ -215,7 +215,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, subtitleIndex: val, - } + }, ) } selected={selectedOptions.subtitleIndex} @@ -224,7 +224,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} @@ -235,24 +235,24 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - + {item.Type !== "Program" && ( <> {item.Type === "Episode" && ( - + )} - + {item.People && item.People.length > 0 && ( - + {item.People.slice(0, 3).map((person, idx) => ( ))} @@ -265,5 +265,5 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( ); - } + }, ); diff --git a/components/ItemHeader.tsx b/components/ItemHeader.tsx index ac6bbe4c..8d218d82 100644 --- a/components/ItemHeader.tsx +++ b/components/ItemHeader.tsx @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import React from "react"; -import { View, ViewProps } from "react-native"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { View, type ViewProps } from "react-native"; import { GenreTags } from "./GenreTags"; -import { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { Ratings } from "./Ratings"; +import { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader"; import { ItemActions } from "./series/SeriesActions"; @@ -15,21 +15,21 @@ export const ItemHeader: React.FC = ({ item, ...props }) => { if (!item) return ( - - - - + + + + ); return ( - - - - + + + + {item.Type === "Episode" && ( diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx index e2570dc8..b0354fa0 100644 --- a/components/ItemTechnicalDetails.tsx +++ b/components/ItemTechnicalDetails.tsx @@ -1,22 +1,23 @@ +import { formatBitrate } from "@/utils/bitrate"; import { Ionicons } from "@expo/vector-icons"; import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetScrollView, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import type { MediaSourceInfo, - type MediaStream, + MediaStream, } from "@jellyfin/sdk/lib/generated-client"; -import React, { useMemo, useRef } from "react"; +import type React from "react"; +import { useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; import { Badge } from "./Badge"; -import { Text } from "./common/Text"; -import { - BottomSheetModal, - BottomSheetBackdropProps, - BottomSheetBackdrop, - BottomSheetView, - BottomSheetScrollView, -} from "@gorhom/bottom-sheet"; import { Button } from "./Button"; -import { useTranslation } from "react-i18next"; -import { formatBitrate } from "@/utils/bitrate"; +import { Text } from "./common/Text"; interface Props { source?: MediaSourceInfo; @@ -27,13 +28,13 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { const { t } = useTranslation(); return ( - - {t("item_card.video")} + + {t("item_card.video")} bottomSheetModalRef.current?.present()}> - + - {t("item_card.more_details")} + {t("item_card.more_details")} = ({ source, ...props }) => { )} > - - - + + + {t("item_card.video")} - + - - + + {t("item_card.audio")} stream.Type === "Audio" + (stream) => stream.Type === "Audio", ) || [] } /> - - + + {t("item_card.subtitles")} stream.Type === "Subtitle" + (stream) => stream.Type === "Subtitle", ) || [] } /> @@ -101,25 +102,25 @@ const SubtitleStreamInfo = ({ subtitleStreams: MediaStream[]; }) => { return ( - + {subtitleStreams.map((stream, index) => ( - - + + {stream.DisplayTitle} - + + } text={stream.Language} /> + } /> @@ -131,40 +132,40 @@ const SubtitleStreamInfo = ({ const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => { return ( - + {audioStreams.map((audioStreams, index) => ( - - + + {audioStreams.DisplayTitle} - + + } text={audioStreams.Language} /> } text={audioStreams.Codec} /> } + variant='gray' + iconLeft={} text={audioStreams.ChannelLayout} /> + } text={formatBitrate(audioStreams.BitRate)} /> @@ -180,48 +181,48 @@ const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => { const videoStream = useMemo(() => { return source.MediaStreams?.find( - (stream) => stream.Type === "Video" + (stream) => stream.Type === "Video", ) as MediaStream; }, [source.MediaStreams]); if (!videoStream) return null; return ( - + } + variant='gray' + iconLeft={} text={formatFileSize(source.Size)} /> } + variant='gray' + iconLeft={} text={`${videoStream.Width}x${videoStream.Height}`} /> + } text={videoStream.VideoRange} /> + } text={videoStream.Codec} /> + } text={formatBitrate(videoStream.BitRate)} /> } + variant='gray' + iconLeft={} text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`} /> @@ -233,6 +234,8 @@ const formatFileSize = (bytes?: number | null) => { const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; if (bytes === 0) return "0 Byte"; - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString()); + const i = Number.parseInt( + Math.floor(Math.log(bytes) / Math.log(1024)).toString(), + ); return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i]; }; diff --git a/components/JellyfinServerDiscovery.tsx b/components/JellyfinServerDiscovery.tsx index dc2c46ad..0ea85a4a 100644 --- a/components/JellyfinServerDiscovery.tsx +++ b/components/JellyfinServerDiscovery.tsx @@ -1,10 +1,10 @@ -import React from "react"; -import { View, Text, TouchableOpacity } from "react-native"; import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery"; +import type React from "react"; +import { useTranslation } from "react-i18next"; +import { Text, TouchableOpacity, View } from "react-native"; import { Button } from "./Button"; import { ListGroup } from "./list/ListGroup"; import { ListItem } from "./list/ListItem"; -import { useTranslation } from "react-i18next"; interface Props { onServerSelect?: (server: { address: string; serverName?: string }) => void; @@ -15,15 +15,17 @@ const JellyfinServerDiscovery: React.FC = ({ onServerSelect }) => { const { t } = useTranslation(); return ( - - {servers.length ? ( - + {servers.map((server) => ( { item: BaseItemDto; @@ -24,9 +24,9 @@ export const MediaSourceSelector: React.FC = ({ const selectedName = useMemo( () => item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find( - (x) => x.Type === "Video" + (x) => x.Type === "Video", )?.DisplayTitle || "", - [item, selected] + [item, selected], ); const { t } = useTranslation(); @@ -54,26 +54,26 @@ export const MediaSourceSelector: React.FC = ({ return ( - - + + {t("item_card.video")} - + {selectedName} = ({ return ( - - {t("item_card.more_with", {name: actor?.Name})} + + {t("item_card.more_with", { name: actor?.Name })} = ({ diff --git a/components/OverviewText.tsx b/components/OverviewText.tsx index b775b1c9..e7ed2e97 100644 --- a/components/OverviewText.tsx +++ b/components/OverviewText.tsx @@ -1,8 +1,8 @@ -import { TouchableOpacity, View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { tc } from "@/utils/textTools"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; interface Props extends ViewProps { text?: string | null; @@ -20,20 +20,22 @@ export const OverviewText: React.FC = ({ if (!text) return null; return ( - - {t("item_card.overview")} + + {t("item_card.overview")} setLimit((prev) => - prev === characterLimit ? text.length : characterLimit + prev === characterLimit ? text.length : characterLimit, ) } > {tc(text, limit)} {text.length > characterLimit && ( - - {limit === characterLimit ? t("item_card.show_more") : t("item_card.show_less")} + + {limit === characterLimit + ? t("item_card.show_more") + : t("item_card.show_less")} )} diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx index 5d7b28e0..2efb8395 100644 --- a/components/ParallaxPage.tsx +++ b/components/ParallaxPage.tsx @@ -1,6 +1,11 @@ import { LinearGradient } from "expo-linear-gradient"; -import { type PropsWithChildren, type ReactElement } from "react"; -import {NativeScrollEvent, NativeSyntheticEvent, View, ViewProps} from "react-native"; +import type { PropsWithChildren, ReactElement } from "react"; +import { + type NativeScrollEvent, + NativeSyntheticEvent, + View, + type ViewProps, +} from "react-native"; import Animated, { interpolate, useAnimatedRef, @@ -35,36 +40,40 @@ export const ParallaxScrollView: React.FC> = ({ translateY: interpolate( scrollOffset.value, [-headerHeight, 0, headerHeight], - [-headerHeight / 2, 0, headerHeight * 0.75] + [-headerHeight / 2, 0, headerHeight * 0.75], ), }, { scale: interpolate( scrollOffset.value, [-headerHeight, 0, headerHeight], - [2, 1, 1] + [2, 1, 1], ), }, ], }; }); - - function isCloseToBottom({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) { - return layoutMeasurement.height + contentOffset.y >= contentSize.height - 20; + function isCloseToBottom({ + layoutMeasurement, + contentOffset, + contentSize, + }: NativeScrollEvent) { + return ( + layoutMeasurement.height + contentOffset.y >= contentSize.height - 20 + ); } return ( - + { - if (isCloseToBottom(e.nativeEvent)) - onEndReached?.() + onScroll={(e) => { + if (isCloseToBottom(e.nativeEvent)) onEndReached?.(); }} > {logo && ( @@ -73,7 +82,7 @@ export const ParallaxScrollView: React.FC> = ({ top: headerHeight - 200, height: 130, }} - className="absolute left-0 w-full z-40 px-4 flex justify-center items-center" + className='absolute left-0 w-full z-40 px-4 flex justify-center items-center' > {logo} @@ -95,7 +104,7 @@ export const ParallaxScrollView: React.FC> = ({ style={{ top: -50, }} - className="relative flex-1 bg-transparent pb-24" + className='relative flex-1 bg-transparent pb-24' > { item: BaseItemDto; @@ -74,7 +74,7 @@ export const PlayButton: React.FC = ({ (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); const onPress = useCallback(async () => { @@ -140,7 +140,7 @@ export const PlayButton: React.FC = ({ console.warn("No URL returned from getStreamUrl", data); Alert.alert( t("player.client_error"), - t("player.could_not_create_stream_for_chromecast") + t("player.could_not_create_stream_for_chromecast"), ); return; } @@ -170,36 +170,36 @@ export const PlayButton: React.FC = ({ ], } : item.Type === "Movie" - ? { - type: "movie", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : { - type: "generic", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - }, + ? { + type: "movie", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + } + : { + type: "generic", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + }, }, startTime: 0, }) @@ -222,7 +222,7 @@ export const PlayButton: React.FC = ({ case cancelButtonIndex: break; } - } + }, ); }, [ item, @@ -243,7 +243,7 @@ export const PlayButton: React.FC = ({ return userData.PlaybackPositionTicks > 0 ? Math.max( (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH + MIN_PLAYBACK_WIDTH, ) : 0; } @@ -260,7 +260,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -273,7 +273,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -294,7 +294,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -302,7 +302,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -310,7 +310,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -318,7 +318,7 @@ export const PlayButton: React.FC = ({ color: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.text, endColor.value.text] + [startColor.value.text, endColor.value.text], ), })); /** @@ -328,13 +328,13 @@ export const PlayButton: React.FC = ({ return ( - + = ({ = ({ borderColor: colorAtom.primary, borderStyle: "solid", }} - className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full " + className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ' > - + {runtimeTicksToMinutes(item?.RunTimeTicks)} - + {client && ( - - + + )} {!client && settings?.openInVLC && ( diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 50be8c13..a68058c5 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -1,14 +1,16 @@ -import { Platform } from "react-native"; +import { useHaptic } from "@/hooks/useHaptic"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { useSettings } from "@/utils/atoms/settings"; import { runtimeTicksToMinutes } from "@/utils/time"; import { useActionSheet } from "@expo/react-native-action-sheet"; import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useRouter } from "expo-router"; import { useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { Alert, TouchableOpacity, View } from "react-native"; import Animated, { Easing, @@ -20,10 +22,8 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { Button } from "./Button"; -import { SelectedOptions } from "./ItemContent"; -import { useTranslation } from "react-i18next"; -import { useHaptic } from "@/hooks/useHaptic"; +import type { Button } from "./Button"; +import type { SelectedOptions } from "./ItemContent"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500; const MIN_PLAYBACK_WIDTH = 15; export const PlayButton: React.FC = ({ - item, - selectedOptions, - ...props - }: Props) => { + item, + selectedOptions, + ...props +}: Props) => { const { showActionSheetWithOptions } = useActionSheet(); const { t } = useTranslation(); @@ -60,7 +60,7 @@ export const PlayButton: React.FC = ({ (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); const onPress = () => { @@ -88,9 +88,9 @@ export const PlayButton: React.FC = ({ if (userData && userData.PlaybackPositionTicks) { return userData.PlaybackPositionTicks > 0 ? Math.max( - (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH - ) + (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, + MIN_PLAYBACK_WIDTH, + ) : 0; } return 0; @@ -106,7 +106,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -119,7 +119,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -140,7 +140,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -148,7 +148,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -156,7 +156,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -164,7 +164,7 @@ export const PlayButton: React.FC = ({ color: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.text, endColor.value.text] + [startColor.value.text, endColor.value.text], ), })); /** @@ -173,13 +173,13 @@ export const PlayButton: React.FC = ({ return ( - + = ({ = ({ borderColor: colorAtom.primary, borderStyle: "solid", }} - className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full " + className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ' > - + {runtimeTicksToMinutes(item?.RunTimeTicks)} - + {settings?.openInVLC && ( diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index 5d738af4..00beb18f 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -1,8 +1,8 @@ import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import React from "react"; -import { View, ViewProps } from "react-native"; +import type React from "react"; +import { View, type ViewProps } from "react-native"; import { RoundButton } from "./RoundButton"; interface Props extends ViewProps { @@ -18,7 +18,7 @@ export const PlayedStatus: React.FC = ({ items, ...props }) => { queryClient.invalidateQueries({ queryKey: ["item", item.Id], }); - }) + }); queryClient.invalidateQueries({ queryKey: ["resumeItems"], }); @@ -51,9 +51,9 @@ export const PlayedStatus: React.FC = ({ items, ...props }) => { { + onPress={async () => { console.log(allPlayed); - await markAsPlayedStatus(!allPlayed) + await markAsPlayedStatus(!allPlayed); }} size={props.size} /> diff --git a/components/PreviousServersList.tsx b/components/PreviousServersList.tsx index 437c756d..ffa310d3 100644 --- a/components/PreviousServersList.tsx +++ b/components/PreviousServersList.tsx @@ -1,9 +1,10 @@ -import React, { useMemo } from "react"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { useMMKVString } from "react-native-mmkv"; import { ListGroup } from "./list/ListGroup"; import { ListItem } from "./list/ListItem"; -import { useTranslation } from "react-i18next"; interface Server { address: string; @@ -29,7 +30,7 @@ export const PreviousServersList: React.FC = ({ return ( - + {previousServers.map((s) => ( = ({ setPreviousServers("[]"); }} title={t("server.clear_button")} - textColor="red" + textColor='red' /> diff --git a/components/ProgressCircle.tsx b/components/ProgressCircle.tsx index 20c4fbd3..f3c0a55c 100644 --- a/components/ProgressCircle.tsx +++ b/components/ProgressCircle.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { View, StyleSheet } from "react-native"; +import type React from "react"; +import { StyleSheet, View } from "react-native"; import { AnimatedCircularProgress } from "react-native-circular-progress"; type ProgressCircleProps = { diff --git a/components/Ratings.tsx b/components/Ratings.tsx index c1c9b060..bb6e6107 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -1,15 +1,18 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { View, ViewProps } from "react-native"; -import { Badge } from "./Badge"; -import { Ionicons } from "@expo/vector-icons"; -import { Image } from "expo-image"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useQuery } from "@tanstack/react-query"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {useMemo} from "react"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useMemo } from "react"; +import { View, type ViewProps } from "react-native"; +import { Badge } from "./Badge"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -18,21 +21,21 @@ interface Props extends ViewProps { export const Ratings: React.FC = ({ item, ...props }) => { if (!item) return null; return ( - + {item.OfficialRating && ( - + )} {item.CommunityRating && ( } + variant='gray' + iconLeft={} /> )} {item.CriticRating && ( = ({ item, ...props }) => { ); }; -export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({ - result, -}) => { +export const JellyserrRatings: React.FC<{ + result: MovieResult | TvResult | TvDetails | MovieDetails; +}> = ({ result }) => { const { jellyseerrApi, getMediaType } = useJellyseerr(); const mediaType = useMemo(() => getMediaType(result), [result]); @@ -76,14 +79,14 @@ export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDet !!result.voteCount || (data?.criticsRating && !!data?.criticsScore) || (data?.audienceRating && !!data?.audienceScore)) && ( - + {data?.criticsRating && !!data?.criticsScore && ( void; diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 45914d9f..8b49def4 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -1,18 +1,23 @@ import MoviePoster from "@/components/posters/MoviePoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; import { useAtom } from "jotai"; import { useMemo } from "react"; -import { ScrollView, TouchableOpacity, View, ViewProps } from "react-native"; -import { Text } from "./common/Text"; +import { useTranslation } from "react-i18next"; +import { + ScrollView, + TouchableOpacity, + View, + type ViewProps, +} from "react-native"; import { ItemCardText } from "./ItemCardText"; import { Loader } from "./Loader"; import { HorizontalScroll } from "./common/HorrizontalScroll"; +import { Text } from "./common/Text"; import { TouchableItemRouter } from "./common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; interface SimilarItemsProps extends ViewProps { itemId?: string | null; @@ -39,17 +44,19 @@ export const SimilarItems: React.FC = ({ return response.data.Items || []; }, enabled: !!api && !!user?.Id, - staleTime: Infinity, + staleTime: Number.POSITIVE_INFINITY, }); const movies = useMemo( () => similarItems?.filter((i) => i.Type === "Movie") || [], - [similarItems] + [similarItems], ); return ( - {t("item_card.similar_items")} + + {t("item_card.similar_items")} + = ({ diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx index ac5051e1..1e74bbb6 100644 --- a/components/SubtitleTrackSelector.tsx +++ b/components/SubtitleTrackSelector.tsx @@ -1,10 +1,10 @@ import { tc } from "@/utils/textTools"; -import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; +import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { useMemo } from "react"; import { Platform, TouchableOpacity, View } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Text } from "./common/Text"; import { useTranslation } from "react-i18next"; +import { Text } from "./common/Text"; interface Props extends React.ComponentProps { source?: MediaSourceInfo; @@ -25,7 +25,7 @@ export const SubtitleTrackSelector: React.FC = ({ const selectedSubtitleSteam = useMemo( () => subtitleStreams?.find((x) => x.Index === selected), - [subtitleStreams, selected] + [subtitleStreams, selected], ); if (subtitleStreams?.length === 0) return null; @@ -34,7 +34,7 @@ export const SubtitleTrackSelector: React.FC = ({ return ( = ({ > - - + + {t("item_card.subtitles")} - - + + {selectedSubtitleSteam ? tc(selectedSubtitleSteam?.DisplayTitle, 7) : t("item_card.none")} @@ -57,8 +57,8 @@ export const SubtitleTrackSelector: React.FC = ({ = ({ item }) => { @@ -7,7 +7,7 @@ export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => { <> {item.UserData?.Played === false && (item.Type === "Movie" || item.Type === "Episode") && ( - + )} ); diff --git a/components/__tests__/ThemedText-test.tsx b/components/__tests__/ThemedText-test.tsx index 1ac32250..591f09e2 100644 --- a/components/__tests__/ThemedText-test.tsx +++ b/components/__tests__/ThemedText-test.tsx @@ -1,10 +1,12 @@ -import * as React from 'react'; -import renderer from 'react-test-renderer'; +import * as React from "react"; +import renderer from "react-test-renderer"; -import { ThemedText } from '../ThemedText'; +import { ThemedText } from "../ThemedText"; it(`renders correctly`, () => { - const tree = renderer.create(Snapshot test!).toJSON(); + const tree = renderer + .create(Snapshot test!) + .toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/components/_template.tsx b/components/_template.tsx index 64e7fc7f..2e3dab21 100644 --- a/components/_template.tsx +++ b/components/_template.tsx @@ -1,5 +1,5 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps {} diff --git a/components/common/ColumnItem.tsx b/components/common/ColumnItem.tsx index 6d6cab2b..ef6d827f 100644 --- a/components/common/ColumnItem.tsx +++ b/components/common/ColumnItem.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { StyleSheet, View, ViewProps } from "react-native"; +import { StyleSheet, View, type ViewProps } from "react-native"; const getItemStyle = (index: number, numColumns: number) => { const alignItems = (() => { @@ -29,7 +29,7 @@ export const ColumnItem = ({ ...rest }: ColumnItemProps) => { return ( - + { data: T[]; @@ -21,7 +21,7 @@ interface Props { multiple?: boolean; } -const Dropdown = ({ +const Dropdown = ({ data, disabled, placeholderText, @@ -47,10 +47,10 @@ const Dropdown = ({ {typeof title === "string" ? ( - - {title} - - + + {title} + + {selected?.length !== undefined ? selected.map(titleExtractor).join(",") : placeholderText} @@ -63,8 +63,8 @@ const Dropdown = ({ ({ } return [ ...prev.filter( - (p) => keyExtractor(p) !== keyExtractor(item) + (p) => keyExtractor(p) !== keyExtractor(item), ), ]; - }) + }); }} > @@ -107,7 +107,7 @@ const Dropdown = ({ {titleExtractor(item)} - ) + ), )} diff --git a/components/common/HeaderBackButton.tsx b/components/common/HeaderBackButton.tsx index 12d8071e..fdf77ec5 100644 --- a/components/common/HeaderBackButton.tsx +++ b/components/common/HeaderBackButton.tsx @@ -1,14 +1,14 @@ +import { Text } from "@/components/common/Text"; +import { Ionicons } from "@expo/vector-icons"; +import { BlurView, type BlurViewProps } from "expo-blur"; +import { useRouter } from "expo-router"; import { Platform, TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, ViewProps, } from "react-native"; -import { Text } from "@/components/common/Text"; -import { useRouter } from "expo-router"; -import { Ionicons } from "@expo/vector-icons"; -import { BlurView, BlurViewProps } from "expo-blur"; interface Props extends BlurViewProps { background?: "blur" | "transparent"; @@ -31,13 +31,13 @@ export const HeaderBackButton: React.FC = ({ @@ -46,14 +46,14 @@ export const HeaderBackButton: React.FC = ({ return ( router.back()} - className=" bg-neutral-800/80 rounded-full p-2" + className=' bg-neutral-800/80 rounded-full p-2' {...touchableOpacityProps} > ); diff --git a/components/common/HorrizontalScroll.tsx b/components/common/HorrizontalScroll.tsx index 2dce75d4..2ba30f94 100644 --- a/components/common/HorrizontalScroll.tsx +++ b/components/common/HorrizontalScroll.tsx @@ -1,6 +1,6 @@ -import { FlashList, FlashListProps } from "@shopify/flash-list"; +import { FlashList, type FlashListProps } from "@shopify/flash-list"; import React, { forwardRef, useImperativeHandle, useRef } from "react"; -import { View, ViewStyle } from "react-native"; +import { View, type ViewStyle } from "react-native"; import { Text } from "./Text"; type PartialExcept = Partial & Pick; @@ -44,7 +44,7 @@ export const HorizontalScroll = forwardRef< noItemsText, ...props }: HorizontalScrollProps, - ref: React.ForwardedRef + ref: React.ForwardedRef, ) => { const flashListRef = useRef>(null); @@ -66,16 +66,16 @@ export const HorizontalScroll = forwardRef< item: T; index: number; }) => ( - + {renderItem(item, index)} ); if (!data || loading) { return ( - - - + + + ); } @@ -95,8 +95,8 @@ export const HorizontalScroll = forwardRef< }} keyExtractor={keyExtractor} ListEmptyComponent={() => ( - - + + {noItemsText || "No data available"} @@ -104,5 +104,5 @@ export const HorizontalScroll = forwardRef< {...props} /> ); - } + }, ); diff --git a/components/common/InfiniteHorrizontalScroll.tsx b/components/common/InfiniteHorrizontalScroll.tsx index f3c504f1..cf522a87 100644 --- a/components/common/InfiniteHorrizontalScroll.tsx +++ b/components/common/InfiniteHorrizontalScroll.tsx @@ -1,13 +1,14 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, } from "@jellyfin/sdk/lib/generated-client/models"; -import { FlashList, FlashListProps } from "@shopify/flash-list"; +import { FlashList, type FlashListProps } from "@shopify/flash-list"; import { useInfiniteQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useAtom } from "jotai"; import React, { useEffect, useMemo } from "react"; -import { View, ViewStyle } from "react-native"; +import { View, type ViewStyle } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -15,7 +16,6 @@ import Animated, { } from "react-native-reanimated"; import { Loader } from "../Loader"; import { Text } from "./Text"; -import { t } from "i18next"; interface HorizontalScrollProps extends Omit, "renderItem" | "data" | "style"> { @@ -70,7 +70,7 @@ export function InfiniteHorizontalScroll({ const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -118,7 +118,7 @@ export function InfiniteHorizontalScroll({ ( - + {renderItem(item, index)} )} @@ -136,8 +136,10 @@ export function InfiniteHorizontalScroll({ }} showsHorizontalScrollIndicator={false} ListEmptyComponent={ - - {t("item_card.no_data_available")} + + + {t("item_card.no_data_available")} + } {...props} diff --git a/components/common/Input.tsx b/components/common/Input.tsx index 637023bc..ff46d2cd 100644 --- a/components/common/Input.tsx +++ b/components/common/Input.tsx @@ -1,32 +1,35 @@ import React from "react"; -import {Platform, TextInput, TextInputProps, TouchableOpacity} from "react-native"; +import { + Platform, + TextInput, + type TextInputProps, + TouchableOpacity, +} from "react-native"; export function Input(props: TextInputProps) { const { style, ...otherProps } = props; const inputRef = React.useRef(null); return Platform.isTV ? ( - inputRef?.current?.focus?.()} - > - - + inputRef?.current?.focus?.()}> + + ) : ( - ) + ); } diff --git a/components/common/ItemImage.tsx b/components/common/ItemImage.tsx index 09d85467..412d6b74 100644 --- a/components/common/ItemImage.tsx +++ b/components/common/ItemImage.tsx @@ -1,11 +1,11 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { getItemImage } from "@/utils/getItemImage"; import { Ionicons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Image, ImageProps } from "expo-image"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Image, type ImageProps } from "expo-image"; import { useAtom } from "jotai"; -import {FC, useMemo} from "react"; -import { View, ViewProps } from "react-native"; +import { type FC, useMemo } from "react"; +import { View, type ViewProps } from "react-native"; interface Props extends ImageProps { item: BaseItemDto; @@ -52,13 +52,13 @@ export const ItemImage: FC = ({ if (!source?.uri) return ( diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index ec1a1801..8222f187 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -1,16 +1,20 @@ -import { useRouter, useSegments } from "expo-router"; -import React, { PropsWithChildren, useCallback, useMemo } from "react"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import * as ContextMenu from "@/components/ContextMenu"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { - hasPermission, - Permission, -} from "@/utils/jellyseerr/server/lib/permissions"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import { + Permission, + hasPermission, +} from "@/utils/jellyseerr/server/lib/permissions"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { useRouter, useSegments } from "expo-router"; +import type React from "react"; +import { type PropsWithChildren, useCallback, useMemo } from "react"; +import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; interface Props extends TouchableOpacityProps { result?: MovieResult | TvResult | MovieDetails | TvDetails; @@ -46,16 +50,13 @@ export const TouchableJellyseerrRouter: React.FC> = ({ ); }, [jellyseerrApi, jellyseerrUser]); - const request = useCallback( - () => { - if (!result) return; - requestMedia(mediaTitle, { - mediaId: result.id, - mediaType, - }) - }, - [jellyseerrApi, result] - ); + const request = useCallback(() => { + if (!result) return; + requestMedia(mediaTitle, { + mediaId: result.id, + mediaType, + }); + }, [jellyseerrApi, result]); if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( @@ -75,7 +76,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, - mediaType + mediaType, }, }); }} @@ -91,10 +92,10 @@ export const TouchableJellyseerrRouter: React.FC> = ({ loop={false} key={"content"} > - Actions + Actions {canRequest && mediaType === MediaType.MOVIE && ( { if (autoApprove) { request(); @@ -102,7 +103,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ }} shouldDismissMenuOnSelect > - + Request > = ({ light: "purple", }, }} - androidIconName="download" + androidIconName='download' /> )} diff --git a/components/common/LargePoster.tsx b/components/common/LargePoster.tsx index 50c2bcab..a42241d7 100644 --- a/components/common/LargePoster.tsx +++ b/components/common/LargePoster.tsx @@ -4,16 +4,16 @@ import { View } from "react-native"; export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => { if (!url) return ( - - + + ); - + return ( - + ); diff --git a/components/common/Text.tsx b/components/common/Text.tsx index 624b9da6..fa82a4dc 100644 --- a/components/common/Text.tsx +++ b/components/common/Text.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { Platform, TextProps } from "react-native"; -import { UITextView } from "react-native-uitextview"; +import { Platform, type TextProps } from "react-native"; import { Text as RNText } from "react-native"; +import { UITextView } from "react-native-uitextview"; export function Text( props: TextProps & { uiTextView?: boolean; - } + }, ) { const { style, ...otherProps } = props; if (Platform.isTV) diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index db89e52d..23cb6dd7 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,13 +1,13 @@ -import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { useFavorite } from "@/hooks/useFavorite"; -import { +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; +import { useActionSheet } from "@expo/react-native-action-sheet"; +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { useRouter, useSegments } from "expo-router"; -import { PropsWithChildren, useCallback } from "react"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; -import { useActionSheet } from "@expo/react-native-action-sheet"; +import { type PropsWithChildren, useCallback } from "react"; +import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -15,7 +15,7 @@ interface Props extends TouchableOpacityProps { export const itemRouter = ( item: BaseItemDto | BaseItemPerson, - from: string + from: string, ) => { if ("CollectionType" in item && item.CollectionType === "livetv") { return `/(auth)/(tabs)/${from}/livetv`; @@ -58,12 +58,24 @@ export const TouchableItemRouter: React.FC> = ({ const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); const { isFavorite, toggleFavorite } = useFavorite(item); - + const from = segments[2]; const showActionSheet = useCallback(() => { - if (!(item.Type === "Movie" || item.Type === "Episode" || item.Type === "Series")) return; - const options = ["Mark as Played", "Mark as Not Played", isFavorite ? "Unmark as Favorite" : "Mark as Favorite", "Cancel"]; + if ( + !( + item.Type === "Movie" || + item.Type === "Episode" || + item.Type === "Series" + ) + ) + return; + const options = [ + "Mark as Played", + "Mark as Not Played", + isFavorite ? "Unmark as Favorite" : "Mark as Favorite", + "Cancel", + ]; const cancelButtonIndex = 3; showActionSheetWithOptions( @@ -77,9 +89,9 @@ export const TouchableItemRouter: React.FC> = ({ } else if (selectedIndex === 1) { await markAsPlayedStatus(false); } else if (selectedIndex === 2) { - toggleFavorite() + toggleFavorite(); } - } + }, ); }, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]); diff --git a/components/common/VerticalSkeleton.tsx b/components/common/VerticalSkeleton.tsx index 1b2b1457..45a87749 100644 --- a/components/common/VerticalSkeleton.tsx +++ b/components/common/VerticalSkeleton.tsx @@ -1,5 +1,5 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps { index: number; @@ -12,18 +12,18 @@ export const VerticalSkeleton: React.FC = ({ index, ...props }) => { style={{ width: "32%", }} - className="flex flex-col" + className='flex flex-col' {...props} > - - - + + + ); }; diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 773efab4..7ab86a90 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -2,7 +2,7 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { storage } from "@/utils/mmkv"; -import { JobStatus } from "@/utils/optimize-server"; +import type { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; import { useMutation, useQueryClient } from "@tanstack/react-query"; @@ -14,9 +14,9 @@ import { ActivityIndicator, Platform, TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, - ViewProps, + type ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; @@ -33,22 +33,22 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { const { processes } = useDownload(); if (processes?.length === 0) return ( - - + + {t("home.downloads.active_download")} - + {t("home.downloads.no_active_downloads")} ); return ( - - + + {t("home.downloads.active_downloads")} - + {processes?.map((p: JobStatus) => ( ))} @@ -89,7 +89,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } else { FFmpegKitProvider.FFmpegKit.cancel(Number(id)); setProcesses((prev: any[]) => - prev.filter((p: { id: string }) => p.id !== id) + prev.filter((p: { id: string }) => p.id !== id), ); } }, @@ -117,7 +117,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { return ( router.push(`/(auth)/items/page?id=${process.item.Id}`)} - className="relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden" + className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden' {...props} > {(process.status === "optimizing" || @@ -133,10 +133,10 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { }} > )} - - + + {base64Image && ( - + { /> )} - - {process.item.Type} - {process.item.Name} - + + {process.item.Type} + {process.item.Name} + {process.item.ProductionYear} - + {process.progress === 0 ? ( ) : ( - {process.progress.toFixed(0)}% + {process.progress.toFixed(0)}% )} {process.speed && ( - {process.speed?.toFixed(2)}x + {process.speed?.toFixed(2)}x )} {eta(process) && ( - + {t("home.downloads.eta", { eta: eta(process) })} )} - - {process.status} + + {process.status} cancelJobMutation.mutate(process.id)} - className="ml-auto" + className='ml-auto' > {cancelJobMutation.isPending ? ( - + ) : ( - + )} {process.status === "completed" && ( - + diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx index 48a52a29..22b00412 100644 --- a/components/downloads/DownloadSize.tsx +++ b/components/downloads/DownloadSize.tsx @@ -1,8 +1,9 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import React, { useEffect, useMemo, useState } from "react"; -import { TextProps } from "react-native"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { useEffect, useMemo, useState } from "react"; +import type { TextProps } from "react-native"; interface DownloadSizeProps extends TextProps { items: BaseItemDto[]; @@ -39,7 +40,7 @@ export const DownloadSize: React.FC = ({ return ( <> - + {sizeText} diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index 53b3ecec..97a9308f 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -1,22 +1,27 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useHaptic } from "@/hooks/useHaptic"; -import React, { useCallback, useMemo } from "react"; -import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { ActionSheetProvider, useActionSheet, } from "@expo/react-native-action-sheet"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { useCallback, useMemo } from "react"; +import { + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; +import { Text } from "@/components/common/Text"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener"; import { useDownload } from "@/providers/DownloadProvider"; import { storage } from "@/utils/mmkv"; -import { Image } from "expo-image"; -import { Ionicons } from "@expo/vector-icons"; -import { Text } from "@/components/common/Text"; import { runtimeTicksToSeconds } from "@/utils/time"; -import { DownloadSize } from "@/components/downloads/DownloadSize"; -import { TouchableItemRouter } from "../common/TouchableItemRouter"; +import { Ionicons } from "@expo/vector-icons"; +import { Image } from "expo-image"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; interface EpisodeCardProps extends TouchableOpacityProps { item: BaseItemDto; @@ -67,7 +72,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { // Cancelled break; } - } + }, ); }, [showActionSheetWithOptions, handleDeleteFile]); @@ -76,27 +81,27 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { onPress={handleOpenFile} onLongPress={showActionSheet} key={item.Id} - className="flex flex-col mb-4" + className='flex flex-col mb-4' > - - - + + + - - + + {item.Name} - + {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(item.RunTimeTicks)} - + {item.Overview} @@ -105,7 +110,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { // Wrap the parent component with ActionSheetProvider export const EpisodeCardWithActionSheet: React.FC = ( - props + props, ) => ( diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index bb61f3c8..e15fd003 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -1,10 +1,11 @@ +import { useHaptic } from "@/hooks/useHaptic"; import { ActionSheetProvider, useActionSheet, } from "@expo/react-native-action-sheet"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useHaptic } from "@/hooks/useHaptic"; -import React, { useCallback, useMemo } from "react"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { useCallback, useMemo } from "react"; import { TouchableOpacity, View } from "react-native"; import { DownloadSize } from "@/components/downloads/DownloadSize"; @@ -69,14 +70,14 @@ export const MovieCard: React.FC = ({ item }) => { // Cancelled break; } - } + }, ); }, [showActionSheetWithOptions, handleDeleteFile]); return ( {base64Image ? ( - + = ({ item }) => { /> ) : ( - + )} - + diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 4c6efa1f..877a5240 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -1,16 +1,17 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import {TouchableOpacity, View} from "react-native"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; +import { useDownload } from "@/providers/DownloadProvider"; +import { storage } from "@/utils/mmkv"; +import { useActionSheet } from "@expo/react-native-action-sheet"; +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Image } from "expo-image"; +import { router } from "expo-router"; +import type React from "react"; +import { useCallback, useMemo } from "react"; +import { TouchableOpacity, View } from "react-native"; import { Text } from "../common/Text"; -import React, {useCallback, useMemo} from "react"; -import {storage} from "@/utils/mmkv"; -import {Image} from "expo-image"; -import {Ionicons} from "@expo/vector-icons"; -import {router} from "expo-router"; -import {DownloadSize} from "@/components/downloads/DownloadSize"; -import {useDownload} from "@/providers/DownloadProvider"; -import {useActionSheet} from "@expo/react-native-action-sheet"; -export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { +export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => { const { deleteItems } = useDownload(); const { showActionSheetWithOptions } = useActionSheet(); @@ -18,16 +19,14 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { return storage.getString(items[0].SeriesId!); }, []); - const deleteSeries = useCallback( - async () => deleteItems(items), - [items] - ); + const deleteSeries = useCallback(async () => deleteItems(items), [items]); const showActionSheet = useCallback(() => { const options = ["Delete", "Cancel"]; const destructiveButtonIndex = 0; - showActionSheetWithOptions({ + showActionSheetWithOptions( + { options, destructiveButtonIndex, }, @@ -35,7 +34,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { if (selectedIndex == destructiveButtonIndex) { deleteSeries(); } - } + }, ); }, [showActionSheetWithOptions, deleteSeries]); @@ -45,7 +44,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { onLongPress={showActionSheet} > {base64Image ? ( - + = ({items}) => { resizeMode: "cover", }} /> - - {items.length} + + {items.length} ) : ( - + )} - - {items[0].SeriesName} - {items[0].ProductionYear} + + + {items[0].SeriesName} + + {items[0].ProductionYear} diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index a96e7348..84bf8bfb 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -2,7 +2,7 @@ import { Text } from "@/components/common/Text"; import { FontAwesome, Ionicons } from "@expo/vector-icons"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { FilterSheet } from "./FilterSheet"; interface FilterButtonProps extends ViewProps { @@ -68,16 +68,16 @@ export const FilterButton = ({ {icon === "filter" ? ( ) : ( )} diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index 7b14af03..f3508ffd 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -1,25 +1,25 @@ import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetFlatList, BottomSheetModal, BottomSheetScrollView, BottomSheetView, } from "@gorhom/bottom-sheet"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Text } from "@/components/common/Text"; -import { StyleSheet, TouchableOpacity, View, ViewProps } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import { useTranslation } from "react-i18next"; +import { + StyleSheet, + TouchableOpacity, + View, + type ViewProps, +} from "react-native"; import { Button } from "../Button"; import { Input } from "../common/Input"; -import { useTranslation } from "react-i18next"; interface Props extends ViewProps { open: boolean; @@ -130,7 +130,7 @@ export const FilterSheet = ({ appearsOnIndex={0} /> ), - [] + [], ); return ( @@ -153,18 +153,20 @@ export const FilterSheet = ({ flex: 1, }} > - - {title} - {t("search.x_items", {count: _data?.length})} + + {title} + + {t("search.x_items", { count: _data?.length })} + {showSearch && ( { setSearch(text); }} - returnKeyType="done" + returnKeyType='done' /> )} ({ borderRadius: 20, overflow: "hidden", }} - className="mb-4 flex flex-col rounded-xl overflow-hidden" + className='mb-4 flex flex-col rounded-xl overflow-hidden' > {renderData?.map((item, index) => ( @@ -185,20 +187,20 @@ export const FilterSheet = ({ }, 250); } }} - className=" bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between" + className=' bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between' > {renderItemLabel(item)} {values.some((i) => i === item) ? ( - + ) : ( - + )} ))} diff --git a/components/filters/ResetFiltersButton.tsx b/components/filters/ResetFiltersButton.tsx index dfeee025..6c48ee3f 100644 --- a/components/filters/ResetFiltersButton.tsx +++ b/components/filters/ResetFiltersButton.tsx @@ -5,7 +5,7 @@ import { } from "@/utils/atoms/filters"; import { Ionicons } from "@expo/vector-icons"; import { useAtom } from "jotai"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; +import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; interface Props extends TouchableOpacityProps {} @@ -29,10 +29,10 @@ export const ResetFiltersButton: React.FC = ({ ...props }) => { setSelectedTags([]); setSelectedYears([]); }} - className="bg-purple-600 rounded-full w-[30px] h-[30px] flex items-center justify-center mr-1" + className='bg-purple-600 rounded-full w-[30px] h-[30px] flex items-center justify-center mr-1' {...props} > - + ); }; diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index fba0ca6c..6fe263a6 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -13,154 +13,154 @@ import { ScrollingCollectionList } from "./ScrollingCollectionList"; import heart from "@/assets/icons/heart.fill.png"; type FavoriteTypes = - | "Series" - | "Movie" - | "Episode" - | "Video" - | "BoxSet" - | "Playlist"; + | "Series" + | "Movie" + | "Episode" + | "Video" + | "BoxSet" + | "Playlist"; type EmptyState = Record; export const Favorites = () => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - const [emptyState, setEmptyState] = useState({ - Series: false, - Movie: false, - Episode: false, - Video: false, - BoxSet: false, - Playlist: false, - }); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const [emptyState, setEmptyState] = useState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); - const fetchFavoritesByType = useCallback( - async (itemType: BaseItemKind) => { - const response = await getItemsApi(api as Api).getItems({ - userId: user?.Id, - sortBy: ["SeriesSortName", "SortName"], - sortOrder: ["Ascending"], - filters: ["IsFavorite"], - recursive: true, - fields: ["PrimaryImageAspectRatio"], - collapseBoxSetItems: false, - excludeLocationTypes: ["Virtual"], - enableTotalRecordCount: false, - limit: 20, - includeItemTypes: [itemType], - }); - const items = response.data.Items || []; + const fetchFavoritesByType = useCallback( + async (itemType: BaseItemKind) => { + const response = await getItemsApi(api as Api).getItems({ + userId: user?.Id, + sortBy: ["SeriesSortName", "SortName"], + sortOrder: ["Ascending"], + filters: ["IsFavorite"], + recursive: true, + fields: ["PrimaryImageAspectRatio"], + collapseBoxSetItems: false, + excludeLocationTypes: ["Virtual"], + enableTotalRecordCount: false, + limit: 20, + includeItemTypes: [itemType], + }); + const items = response.data.Items || []; - // Update empty state for this specific type - setEmptyState((prev) => ({ - ...prev, - [itemType as FavoriteTypes]: items.length === 0, - })); + // Update empty state for this specific type + setEmptyState((prev) => ({ + ...prev, + [itemType as FavoriteTypes]: items.length === 0, + })); - return items; - }, - [api, user], - ); + return items; + }, + [api, user], + ); - // Reset empty state when component mounts or dependencies change - useEffect(() => { - setEmptyState({ - Series: false, - Movie: false, - Episode: false, - Video: false, - BoxSet: false, - Playlist: false, - }); - }, [api, user]); + // Reset empty state when component mounts or dependencies change + useEffect(() => { + setEmptyState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); + }, [api, user]); - // Check if all categories that have been loaded are empty - const areAllEmpty = () => { - const loadedCategories = Object.values(emptyState); - return ( - loadedCategories.length > 0 && - loadedCategories.every((isEmpty) => isEmpty) - ); - }; + // Check if all categories that have been loaded are empty + const areAllEmpty = () => { + const loadedCategories = Object.values(emptyState); + return ( + loadedCategories.length > 0 && + loadedCategories.every((isEmpty) => isEmpty) + ); + }; - const fetchFavoriteSeries = useCallback( - () => fetchFavoritesByType("Series"), - [fetchFavoritesByType], - ); - const fetchFavoriteMovies = useCallback( - () => fetchFavoritesByType("Movie"), - [fetchFavoritesByType], - ); - const fetchFavoriteEpisodes = useCallback( - () => fetchFavoritesByType("Episode"), - [fetchFavoritesByType], - ); - const fetchFavoriteVideos = useCallback( - () => fetchFavoritesByType("Video"), - [fetchFavoritesByType], - ); - const fetchFavoriteBoxsets = useCallback( - () => fetchFavoritesByType("BoxSet"), - [fetchFavoritesByType], - ); - const fetchFavoritePlaylists = useCallback( - () => fetchFavoritesByType("Playlist"), - [fetchFavoritesByType], - ); + const fetchFavoriteSeries = useCallback( + () => fetchFavoritesByType("Series"), + [fetchFavoritesByType], + ); + const fetchFavoriteMovies = useCallback( + () => fetchFavoritesByType("Movie"), + [fetchFavoritesByType], + ); + const fetchFavoriteEpisodes = useCallback( + () => fetchFavoritesByType("Episode"), + [fetchFavoritesByType], + ); + const fetchFavoriteVideos = useCallback( + () => fetchFavoritesByType("Video"), + [fetchFavoritesByType], + ); + const fetchFavoriteBoxsets = useCallback( + () => fetchFavoritesByType("BoxSet"), + [fetchFavoritesByType], + ); + const fetchFavoritePlaylists = useCallback( + () => fetchFavoritesByType("Playlist"), + [fetchFavoritesByType], + ); - return ( - - {areAllEmpty() && ( - - - - {t("favorites.noDataTitle")} - - - {t("favorites.noData")} - - - )} - - - - - - - - ); + return ( + + {areAllEmpty() && ( + + + + {t("favorites.noDataTitle")} + + + {t("favorites.noData")} + + + )} + + + + + + + + ); }; diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index 5b228901..9b0851ef 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -3,14 +3,14 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useRouter, useSegments } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; -import { Dimensions, View, ViewProps } from "react-native"; +import { Dimensions, View, type ViewProps } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, @@ -18,7 +18,7 @@ import Animated, { withTiming, } from "react-native-reanimated"; import Carousel, { - ICarouselInstance, + type ICarouselInstance, Pagination, } from "react-native-reanimated-carousel"; import { itemRouter } from "../common/TouchableItemRouter"; @@ -88,13 +88,13 @@ export const LargeMovieCarousel: React.FC = ({ ...props }) => { if (!popularItems) return null; return ( - + = ({ item }) => { style={{ opacity: opacity, }} - className="px-4" + className='px-4' > - + = ({ item }) => { overflow: "hidden", }} /> - + = ({ return ( - + {title} {isLoading === false && data?.length === 0 && ( - - {t("home.no_items")} + + {t("home.no_items")} )} {isLoading ? ( @@ -62,19 +62,19 @@ export const ScrollingCollectionList: React.FC = ({ `} > {[1, 2, 3].map((i) => ( - - - + + + Nisi mollit voluptate amet. - + Lorem ipsum @@ -85,7 +85,7 @@ export const ScrollingCollectionList: React.FC = ({ ) : ( - + {data?.map((item) => ( void, - appendValue?: string, + value: number; + disabled?: boolean; + step: number; + min: number; + max: number; + onUpdate: (value: number) => void; + appendValue?: string; } export const Stepper: React.FC = ({ @@ -19,33 +19,35 @@ export const Stepper: React.FC = ({ min, max, onUpdate, - appendValue + appendValue, }) => { return ( onUpdate(Math.max(min, value - step))} - className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" + className='w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center' > - - {value}{appendValue} + {value} + {appendValue} onUpdate(Math.min(max, value + step))} > + - ) -} \ No newline at end of file + ); +}; diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx index 8dcdd785..27e8201c 100644 --- a/components/jellyseerr/Cast.tsx +++ b/components/jellyseerr/Cast.tsx @@ -1,11 +1,11 @@ -import { View, ViewProps } from "react-native"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import React from "react"; -import { FlashList } from "@shopify/flash-list"; import { Text } from "@/components/common/Text"; import PersonPoster from "@/components/jellyseerr/PersonPoster"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { FlashList } from "@shopify/flash-list"; +import type React from "react"; import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; const CastSlide: React.FC< { details?: MovieDetails | TvDetails } & ViewProps @@ -15,12 +15,14 @@ const CastSlide: React.FC< details?.credits?.cast && details?.credits?.cast?.length > 0 && ( - {t("jellyseerr.cast")} + + {t("jellyseerr.cast")} + } + ItemSeparatorComponent={() => } estimatedItemSize={15} keyExtractor={(item) => item?.id?.toString()} contentContainerStyle={{ paddingHorizontal: 16 }} diff --git a/components/jellyseerr/DetailFacts.tsx b/components/jellyseerr/DetailFacts.tsx index e6ef013a..4e9e1580 100644 --- a/components/jellyseerr/DetailFacts.tsx +++ b/components/jellyseerr/DetailFacts.tsx @@ -1,15 +1,15 @@ -import { View, ViewProps } from "react-native"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Text } from "@/components/common/Text"; -import { useMemo } from "react"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { uniqBy } from "lodash"; -import { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; -import CountryFlag from "react-native-country-flag"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; +import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { uniqBy } from "lodash"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; +import CountryFlag from "react-native-country-flag"; interface Release { certification: string; @@ -30,12 +30,12 @@ const Facts: React.FC< > = ({ title, facts, ...props }) => facts && facts?.length > 0 && ( - - {title} + + {title} - + {facts.map((f, idx) => - typeof f === "string" ? {f} : f + typeof f === "string" ? {f} : f, )} @@ -50,15 +50,19 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({ const DetailFacts: React.FC< { details?: MovieDetails | TvDetails } & ViewProps > = ({ details, className, ...props }) => { - const { jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrUser, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const { t } = useTranslation(); const releases = useMemo( () => (details as MovieDetails)?.releases?.results.find( - (r: TmdbRelease) => r.iso_3166_1 === region + (r: TmdbRelease) => r.iso_3166_1 === region, )?.release_dates as TmdbRelease["release_dates"], - [details] + [details], ); // Release date types: @@ -72,9 +76,9 @@ const DetailFacts: React.FC< () => uniqBy( releases?.filter((r: Release) => r.type > 2 && r.type < 6), - "type" + "type", ), - [releases] + [releases], ); const firstAirDate = useMemo(() => { @@ -82,7 +86,7 @@ const DetailFacts: React.FC< if (firstAirDate) { return new Date(firstAirDate).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, ); } }, [details]); @@ -93,7 +97,7 @@ const DetailFacts: React.FC< if (nextAirDate && firstAirDate !== nextAirDate) { return new Date(nextAirDate).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, ); } }, [details]); @@ -102,26 +106,26 @@ const DetailFacts: React.FC< () => (details as MovieDetails)?.revenue?.toLocaleString?.( `${locale}-${region}`, - { style: "currency", currency: "USD" } + { style: "currency", currency: "USD" }, ), - [details] + [details], ); const budget = useMemo( () => (details as MovieDetails)?.budget?.toLocaleString?.( `${locale}-${region}`, - { style: "currency", currency: "USD" } + { style: "currency", currency: "USD" }, ), - [details] + [details], ); const streamingProviders = useMemo( () => details?.watchProviders?.find( - (provider) => provider.iso_3166_1 === region + (provider) => provider.iso_3166_1 === region, )?.flatrate, - [details] + [details], ); const networks = useMemo(() => (details as TvDetails)?.networks, [details]); @@ -129,15 +133,15 @@ const DetailFacts: React.FC< const spokenLanguage = useMemo( () => details?.spokenLanguages.find( - (lng) => lng.iso_639_1 === details.originalLanguage + (lng) => lng.iso_639_1 === details.originalLanguage, )?.name, - [details] + [details], ); return ( details && ( - - {t("jellyseerr.details")} + + {t("jellyseerr.details")} {details.keywords.some( - (keyword) => keyword.id === ANIME_KEYWORD_ID - ) && } + (keyword) => keyword.id === ANIME_KEYWORD_ID, + ) && } ( - + {r.type === 3 ? ( // Theatrical - + ) : r.type === 4 ? ( // Digital - + ) : ( // Physical )} {new Date(r.release_date).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, )} @@ -181,11 +185,14 @@ const DetailFacts: React.FC< - + ( - + {n.name} @@ -194,10 +201,13 @@ const DetailFacts: React.FC< n.name + (n) => n.name, )} /> - n.name)} /> + n.name)} + /> s.name)} diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 51aeb938..e2467b31 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -1,14 +1,17 @@ import Discover from "@/components/jellyseerr/discover/Discover"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { +import type { MovieResult, PersonResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; -import React, {useMemo, useState} from "react"; -import { View, ViewProps } from "react-native"; +import { orderBy, uniqBy } from "lodash"; +import type React from "react"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; import { useAnimatedReaction, useAnimatedStyle, @@ -20,8 +23,6 @@ import JellyseerrPoster from "../posters/JellyseerrPoster"; import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; -import { useTranslation } from "react-i18next"; -import {orderBy, uniqBy} from "lodash"; interface Props extends ViewProps { searchQuery: string; @@ -30,15 +31,15 @@ interface Props extends ViewProps { } export enum JellyseerrSearchSort { - DEFAULT, - VOTE_COUNT_AND_AVERAGE, - POPULARITY + DEFAULT = 0, + VOTE_COUNT_AND_AVERAGE = 1, + POPULARITY = 2, } export const JellyserrIndexPage: React.FC = ({ searchQuery, sortType, - order + order, }) => { const { jellyseerrApi } = useJellyseerr(); const opacity = useSharedValue(1); @@ -57,19 +58,24 @@ export const JellyserrIndexPage: React.FC = ({ const { data: jellyseerrResults, isFetching: f2, - isLoading: l2 + isLoading: l2, } = useReactNavigationQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], queryFn: async () => { const params = { - query: new URLSearchParams(searchQuery || "").toString() - } + query: new URLSearchParams(searchQuery || "").toString(), + }; return await Promise.all([ - jellyseerrApi?.search({...params, page: 1}), - jellyseerrApi?.search({...params, page: 2}), - jellyseerrApi?.search({...params, page: 3}), - jellyseerrApi?.search({...params, page: 4}) - ]).then(all => uniqBy(all.flatMap(v => v?.results || []), "id")) + jellyseerrApi?.search({ ...params, page: 1 }), + jellyseerrApi?.search({ ...params, page: 2 }), + jellyseerrApi?.search({ ...params, page: 3 }), + jellyseerrApi?.search({ ...params, page: 4 }), + ]).then((all) => + uniqBy( + all.flatMap((v) => v?.results || []), + "id", + ), + ); }, enabled: !!jellyseerrApi && searchQuery.length > 0, }); @@ -82,57 +88,66 @@ export const JellyserrIndexPage: React.FC = ({ } else { opacity.value = withTiming(0, { duration: 200 }); } - } + }, ); - const sortingType = useMemo( - () => { - if (!sortType) return; - switch (Number(JellyseerrSearchSort[sortType])) { - case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: - return ["voteCount", "voteAverage"]; - case JellyseerrSearchSort.POPULARITY: - return ["voteCount", "popularity"] - default: - return undefined - } - }, - [sortType, order] - ) + const sortingType = useMemo(() => { + if (!sortType) return; + switch (Number(JellyseerrSearchSort[sortType])) { + case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: + return ["voteCount", "voteAverage"]; + case JellyseerrSearchSort.POPULARITY: + return ["voteCount", "popularity"]; + default: + return undefined; + } + }, [sortType, order]); const jellyseerrMovieResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[], - sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.MOVIE, + ) as MovieResult[], + sortingType || [ + (m) => m.title.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); const jellyseerrTvResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], - sortingType || [t => t.name.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.TV, + ) as TvResult[], + sortingType || [ + (t) => t.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); const jellyseerrPersonResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[], - sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === "person", + ) as PersonResult[], + sortingType || [ + (p) => p.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); if (!searchQuery.length) return ( - + ); @@ -149,10 +164,10 @@ export const JellyserrIndexPage: React.FC = ({ !l1 && !l2 && ( - + {t("search.no_results_found_for")} - + "{searchQuery}" @@ -178,7 +193,7 @@ export const JellyserrIndexPage: React.FC = ({ items={jellyseerrPersonResults} renderItem={(item: PersonResult) => ( = ({ - mediaType, - className, - ...props -}) => { +const JellyseerrMediaIcon: React.FC< + { mediaType: "tv" | "movie" } & ViewProps +> = ({ mediaType, className, ...props }) => { const style = useMemo( - () => mediaType === MediaType.MOVIE - ? 'bg-blue-600/90 border-blue-400/40' - : 'bg-purple-600/90 border-purple-400/40', - [mediaType] + () => + mediaType === MediaType.MOVIE + ? "bg-blue-600/90 border-blue-400/40" + : "bg-purple-600/90 border-purple-400/40", + [mediaType], ); return ( - mediaType && - - {mediaType === MediaType.MOVIE ? ( - - ) : ( - - )} - - ) -} + mediaType && ( + + {mediaType === MediaType.MOVIE ? ( + + ) : ( + + )} + + ) + ); +}; -export default JellyseerrMediaIcon; \ No newline at end of file +export default JellyseerrMediaIcon; diff --git a/components/jellyseerr/JellyseerrStatusIcon.tsx b/components/jellyseerr/JellyseerrStatusIcon.tsx index 8fc593fa..26452a91 100644 --- a/components/jellyseerr/JellyseerrStatusIcon.tsx +++ b/components/jellyseerr/JellyseerrStatusIcon.tsx @@ -1,7 +1,7 @@ -import {useEffect, useState} from "react"; -import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; -import {MaterialCommunityIcons} from "@expo/vector-icons"; -import {TouchableOpacity, View, ViewProps} from "react-native"; +import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import { useEffect, useState } from "react"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; interface Props { mediaStatus?: MediaStatus; @@ -15,7 +15,8 @@ const JellyseerrStatusIcon: React.FC = ({ onPress, ...props }) => { - const [badgeIcon, setBadgeIcon] = useState(); + const [badgeIcon, setBadgeIcon] = + useState(); const [badgeStyle, setBadgeStyle] = useState(); // Match similar to what Jellyseerr is currently using @@ -23,49 +24,54 @@ const JellyseerrStatusIcon: React.FC = ({ useEffect(() => { switch (mediaStatus) { case MediaStatus.PROCESSING: - setBadgeStyle('bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100'); - setBadgeIcon('clock'); + setBadgeStyle( + "bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100", + ); + setBadgeIcon("clock"); break; case MediaStatus.AVAILABLE: - setBadgeStyle('bg-purple-500 border-green-400 ring-green-400 text-green-100'); - setBadgeIcon('check') + setBadgeStyle( + "bg-purple-500 border-green-400 ring-green-400 text-green-100", + ); + setBadgeIcon("check"); break; case MediaStatus.PENDING: - setBadgeStyle('bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100'); - setBadgeIcon('bell') + setBadgeStyle( + "bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100", + ); + setBadgeIcon("bell"); break; case MediaStatus.BLACKLISTED: - setBadgeStyle('bg-red-500 border-white-400 ring-white-400 text-white'); - setBadgeIcon('eye-off') + setBadgeStyle("bg-red-500 border-white-400 ring-white-400 text-white"); + setBadgeIcon("eye-off"); break; case MediaStatus.PARTIALLY_AVAILABLE: - setBadgeStyle('bg-green-500 border-green-400 ring-green-400 text-green-100'); + setBadgeStyle( + "bg-green-500 border-green-400 ring-green-400 text-green-100", + ); setBadgeIcon("minus"); break; default: if (showRequestIcon) { - setBadgeStyle('bg-green-600'); - setBadgeIcon("plus") + setBadgeStyle("bg-green-600"); + setBadgeIcon("plus"); } break; } - }, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon]) + }, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon]); return ( - badgeIcon && - + badgeIcon && ( + - + - - ) -} + + ) + ); +}; -export default JellyseerrStatusIcon; \ No newline at end of file +export default JellyseerrStatusIcon; diff --git a/components/jellyseerr/ParallaxSlideShow.tsx b/components/jellyseerr/ParallaxSlideShow.tsx index 6a7fcb7f..1e7a6142 100644 --- a/components/jellyseerr/ParallaxSlideShow.tsx +++ b/components/jellyseerr/ParallaxSlideShow.tsx @@ -1,29 +1,27 @@ -import React, { - PropsWithChildren, +import { ParallaxScrollView } from "@/components/ParallaxPage"; +import { Text } from "@/components/common/Text"; +import { FlashList } from "@shopify/flash-list"; +import { useFocusEffect } from "expo-router"; +import type React from "react"; +import { + type PropsWithChildren, useCallback, useEffect, useRef, useState, } from "react"; -import {Dimensions, View, ViewProps} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import { Text } from "@/components/common/Text"; +import { Dimensions, View, type ViewProps } from "react-native"; import { Animated } from "react-native"; -import { FlashList } from "@shopify/flash-list"; -import {useFocusEffect} from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; const ANIMATION_ENTER = 250; const ANIMATION_EXIT = 250; const BACKDROP_DURATION = 5000; -type Render = React.ComponentType - | React.ReactElement - | null - | undefined; +type Render = React.ComponentType | React.ReactElement | null | undefined; interface Props { - data: T[] + data: T[]; images: string[]; logo?: React.ReactElement; HeaderContent?: () => React.ReactElement; @@ -34,7 +32,7 @@ interface Props { onEndReached?: (() => void) | null | undefined; } -const ParallaxSlideShow = ({ +const ParallaxSlideShow = ({ data, images, logo, @@ -45,8 +43,7 @@ const ParallaxSlideShow = ({ keyExtractor, onEndReached, ...props -}: PropsWithChildren & ViewProps> -) => { +}: PropsWithChildren & ViewProps>) => { const insets = useSafeAreaInsets(); const [currentIndex, setCurrentIndex] = useState(0); @@ -59,7 +56,7 @@ const ParallaxSlideShow = ({ duration: ANIMATION_ENTER, useNativeDriver: true, }), - [fadeAnim] + [fadeAnim], ); const exitAnimation = useCallback( @@ -69,7 +66,7 @@ const ParallaxSlideShow = ({ duration: ANIMATION_EXIT, useNativeDriver: true, }), - [fadeAnim] + [fadeAnim], ); useEffect(() => { @@ -77,31 +74,35 @@ const ParallaxSlideShow = ({ enterAnimation().start(); const intervalId = setInterval(() => { - Animated.sequence([ - enterAnimation(), - exitAnimation() - ]).start(() => { + Animated.sequence([enterAnimation(), exitAnimation()]).start(() => { fadeAnim.setValue(0); setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length); - }) + }); }, BACKDROP_DURATION); return () => { - clearInterval(intervalId) + clearInterval(intervalId); }; } - }, [fadeAnim, images, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]); + }, [ + fadeAnim, + images, + enterAnimation, + exitAnimation, + setCurrentIndex, + currentIndex, + ]); return ( ({ } logo={logo} > - - - + + + {HeaderContent && HeaderContent()} @@ -131,30 +132,30 @@ const ParallaxSlideShow = ({ - + + No results } - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' ListHeaderComponent={ - {listHeader} + {listHeader} } nestedScrollEnabled showsVerticalScrollIndicator={false} //@ts-ignore - renderItem={({ item, index}) => renderItem(item, index)} + renderItem={({ item, index }) => renderItem(item, index)} keyExtractor={keyExtractor} numColumns={3} estimatedItemSize={214} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => } /> ); -} +}; -export default ParallaxSlideShow; \ No newline at end of file +export default ParallaxSlideShow; diff --git a/components/jellyseerr/PersonPoster.tsx b/components/jellyseerr/PersonPoster.tsx index 6e7d9aa6..075fedf2 100644 --- a/components/jellyseerr/PersonPoster.tsx +++ b/components/jellyseerr/PersonPoster.tsx @@ -1,15 +1,15 @@ -import {TouchableOpacity, View, ViewProps} from "react-native"; -import React from "react"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; import Poster from "@/components/posters/Poster"; -import {useRouter, useSegments} from "expo-router"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useRouter, useSegments } from "expo-router"; +import type React from "react"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; interface Props { - id: string - posterPath?: string - name: string - subName?: string + id: string; + posterPath?: string; + name: string; + subName?: string; } const PersonPoster: React.FC = ({ @@ -19,24 +19,28 @@ const PersonPoster: React.FC = ({ subName, ...props }) => { - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); const router = useRouter(); const segments = useSegments(); const from = segments[2]; if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)}> - + + router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`) + } + > + - {name} - {subName && {subName}} + {name} + {subName && {subName}} - ) -} + ); +}; -export default PersonPoster; \ No newline at end of file +export default PersonPoster; diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx index 192d2d83..a777dde3 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -1,21 +1,30 @@ -import React, {forwardRef, useCallback, useMemo, useState} from "react"; -import {View, ViewProps} from "react-native"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {useQuery} from "@tanstack/react-query"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; -import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet"; +import { Button } from "@/components/Button"; import Dropdown from "@/components/common/Dropdown"; -import {QualityProfile, RootFolder, Tag} from "@/utils/jellyseerr/server/api/servarr/base"; -import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types"; -import {Button} from "@/components/Button"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import type { + QualityProfile, + RootFolder, + Tag, +} from "@/utils/jellyseerr/server/api/servarr/base"; +import type { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types"; +import { useQuery } from "@tanstack/react-query"; +import React, { forwardRef, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; interface Props { id: number; - title: string, - requestBody?: MediaRequestBody, + title: string; + requestBody?: MediaRequestBody; type: MediaType; isAnime?: boolean; is4k?: boolean; @@ -23,216 +32,252 @@ interface Props { onDismiss?: () => void; } -const RequestModal = forwardRef>(({ - id, - title, - requestBody, - type, - isAnime = false, - onRequested, - onDismiss, - ...props -}, ref) => { - const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr(); - const [requestOverrides, setRequestOverrides] = - useState({ +const RequestModal = forwardRef< + BottomSheetModalMethods, + Props & Omit +>( + ( + { + id, + title, + requestBody, + type, + isAnime = false, + onRequested, + onDismiss, + ...props + }, + ref, + ) => { + const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); + const [requestOverrides, setRequestOverrides] = useState({ mediaId: Number(id), mediaType: type, - userId: jellyseerrUser?.id + userId: jellyseerrUser?.id, }); - const { t } = useTranslation(); + const { t } = useTranslation(); - const {data: serviceSettings} = useQuery({ - queryKey: ["jellyseerr", "request", type, 'service'], - queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'), - enabled: !!jellyseerrApi && !!jellyseerrUser, - refetchOnMount: 'always' - }); + const { data: serviceSettings } = useQuery({ + queryKey: ["jellyseerr", "request", type, "service"], + queryFn: async () => + jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"), + enabled: !!jellyseerrApi && !!jellyseerrUser, + refetchOnMount: "always", + }); - const {data: users} = useQuery({ - queryKey: ["jellyseerr", "users"], - queryFn: async () => jellyseerrApi?.user({take: 1000, sort: 'displayname'}), - enabled: !!jellyseerrApi && !!jellyseerrUser, - refetchOnMount: 'always' - }); + const { data: users } = useQuery({ + queryKey: ["jellyseerr", "users"], + queryFn: async () => + jellyseerrApi?.user({ take: 1000, sort: "displayname" }), + enabled: !!jellyseerrApi && !!jellyseerrUser, + refetchOnMount: "always", + }); - const defaultService = useMemo( - () => serviceSettings?.find?.(v => v.isDefault), - [serviceSettings] - ); + const defaultService = useMemo( + () => serviceSettings?.find?.((v) => v.isDefault), + [serviceSettings], + ); - const {data: defaultServiceDetails} = useQuery({ - queryKey: ["jellyseerr", "request", type, 'service', 'details', defaultService?.id], - queryFn: async () => { - setRequestOverrides((prev) => ({ - ...prev, - serverId: defaultService?.id - })) - return jellyseerrApi?.serviceDetails(type === 'movie' ? 'radarr' : 'sonarr', defaultService!!.id) - }, - enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService, - refetchOnMount: 'always', - }); + const { data: defaultServiceDetails } = useQuery({ + queryKey: [ + "jellyseerr", + "request", + type, + "service", + "details", + defaultService?.id, + ], + queryFn: async () => { + setRequestOverrides((prev) => ({ + ...prev, + serverId: defaultService?.id, + })); + return jellyseerrApi?.serviceDetails( + type === "movie" ? "radarr" : "sonarr", + defaultService!.id, + ); + }, + enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService, + refetchOnMount: "always", + }); - const defaultProfile: QualityProfile = useMemo( - () => defaultServiceDetails?.profiles - .find(p => - p.id === (isAnime ? defaultServiceDetails.server?.activeAnimeProfileId : defaultServiceDetails.server?.activeProfileId) - ), - [defaultServiceDetails] - ); + const defaultProfile: QualityProfile = useMemo( + () => + defaultServiceDetails?.profiles.find( + (p) => + p.id === + (isAnime + ? defaultServiceDetails.server?.activeAnimeProfileId + : defaultServiceDetails.server?.activeProfileId), + ), + [defaultServiceDetails], + ); - const defaultFolder: RootFolder = useMemo( - () => defaultServiceDetails?.rootFolders - .find(f => - f.path === (isAnime ? defaultServiceDetails?.server.activeAnimeDirectory : defaultServiceDetails.server?.activeDirectory) - ), - [defaultServiceDetails] - ); + const defaultFolder: RootFolder = useMemo( + () => + defaultServiceDetails?.rootFolders.find( + (f) => + f.path === + (isAnime + ? defaultServiceDetails?.server.activeAnimeDirectory + : defaultServiceDetails.server?.activeDirectory), + ), + [defaultServiceDetails], + ); - const defaultTags: Tag[] = useMemo( - () => { - const tags = defaultServiceDetails?.tags - .filter(t => + const defaultTags: Tag[] = useMemo(() => { + const tags = + defaultServiceDetails?.tags.filter((t) => (isAnime ? defaultServiceDetails?.server.activeAnimeTags : defaultServiceDetails?.server.activeTags - )?.includes(t.id) - ) ?? [] - return tags - }, - [defaultServiceDetails] - ); + )?.includes(t.id), + ) ?? []; + return tags; + }, [defaultServiceDetails]); - const seasonTitle = useMemo( - () => { + const seasonTitle = useMemo(() => { if (requestBody?.seasons && requestBody?.seasons?.length > 1) { - return t("jellyseerr.season_all") + return t("jellyseerr.season_all"); } - return t("jellyseerr.season_number", {season_number: requestBody?.seasons}) - }, - [requestBody?.seasons] - ); + return t("jellyseerr.season_number", { + season_number: requestBody?.seasons, + }); + }, [requestBody?.seasons]); - const request = useCallback(() => {requestMedia( - seasonTitle ? `${title}, ${seasonTitle}` : title, - { - is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, - profileId: defaultProfile.id, - rootFolder: defaultFolder.path, - tags: defaultTags.map(t => t.id), - ...requestBody, - ...requestOverrides - }, - onRequested - ) - }, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]); + const request = useCallback(() => { + requestMedia( + seasonTitle ? `${title}, ${seasonTitle}` : title, + { + is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, + profileId: defaultProfile.id, + rootFolder: defaultFolder.path, + tags: defaultTags.map((t) => t.id), + ...requestBody, + ...requestOverrides, + }, + onRequested, + ); + }, [ + requestBody, + requestOverrides, + defaultProfile, + defaultFolder, + defaultTags, + ]); - const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`; + const pathTitleExtractor = (item: RootFolder) => + `${item.path} (${item.freeSpace.bytesToReadable()})`; - return ( - - - } - > - - - - {t("jellyseerr.advanced")} - {seasonTitle && - {seasonTitle} - } + return ( + ( + + )} + > + + + + + {t("jellyseerr.advanced")} + + {seasonTitle && ( + {seasonTitle} + )} + + + {defaultService && defaultServiceDetails && users && ( + <> + item.name} + placeholderText={ + requestOverrides.profileName || defaultProfile.name + } + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.quality_profile")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + profileId: item?.id, + })) + } + title={t("jellyseerr.quality_profile")} + /> + item.id.toString()} + label={t("jellyseerr.root_folder")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + rootFolder: item.path, + })) + } + title={t("jellyseerr.root_folder")} + /> + item.label} + placeholderText={defaultTags.map((t) => t.label).join(",")} + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.tags")} + onSelected={(...selected) => + setRequestOverrides((prev) => ({ + ...prev, + tags: selected.map((i) => i.id), + })) + } + title={t("jellyseerr.tags")} + /> + item.displayName} + placeholderText={jellyseerrUser!.displayName} + keyExtractor={(item) => item.id.toString() || ""} + label={t("jellyseerr.request_as")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + userId: item?.id, + })) + } + title={t("jellyseerr.request_as")} + /> + + )} + + - - {(defaultService && defaultServiceDetails && users) && ( - <> - item.name} - placeholderText={requestOverrides.profileName || defaultProfile.name} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.quality_profile")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - profileId: item?.id - })) - } - title={t("jellyseerr.quality_profile")} - /> - item.id.toString()} - label={t("jellyseerr.root_folder")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - rootFolder: item.path - }))} - title={t("jellyseerr.root_folder")} - /> - item.label} - placeholderText={defaultTags.map(t => t.label).join(",")} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.tags")} - onSelected={(...selected) => - setRequestOverrides((prev) => ({ - ...prev, - tags: selected.map(i => i.id) - })) - } - title={t("jellyseerr.tags")} - /> - item.displayName} - placeholderText={jellyseerrUser!!.displayName} - keyExtractor={(item) => item.id.toString() || ""} - label={t("jellyseerr.request_as")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - userId: item?.id - })) - } - title={t("jellyseerr.request_as")} - /> - - ) - } - - - - - - ); -}); + + + ); + }, +); -export default RequestModal; \ No newline at end of file +export default RequestModal; diff --git a/components/jellyseerr/discover/CompanySlide.tsx b/components/jellyseerr/discover/CompanySlide.tsx index abee4a9d..fab70288 100644 --- a/components/jellyseerr/discover/CompanySlide.tsx +++ b/components/jellyseerr/discover/CompanySlide.tsx @@ -1,14 +1,15 @@ import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; -import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { COMPANY_LOGO_IMAGE_FILTER, - Network, + type Network, } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; +import type { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import { router, useSegments } from "expo-router"; -import React, { useCallback } from "react"; -import { TouchableOpacity, ViewProps } from "react-native"; +import type React from "react"; +import { useCallback } from "react"; +import { TouchableOpacity, type ViewProps } from "react-native"; const CompanySlide: React.FC< { data: Network[] | Studio[] } & SlideProps & ViewProps @@ -23,7 +24,7 @@ const CompanySlide: React.FC< pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`, params: { id, image, name, type: slide.type }, }), - [slide] + [slide], ); return ( @@ -33,13 +34,13 @@ const CompanySlide: React.FC< data={data} keyExtractor={(item) => item.id.toString()} renderItem={(item, index) => ( - navigate(item)}> + navigate(item)}> diff --git a/components/jellyseerr/discover/Discover.tsx b/components/jellyseerr/discover/Discover.tsx index 847b7e7b..fb3c0084 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/jellyseerr/discover/Discover.tsx @@ -1,50 +1,69 @@ -import React, {useMemo} from "react"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; -import {sortBy} from "lodash"; -import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide"; import CompanySlide from "@/components/jellyseerr/discover/CompanySlide"; -import {View} from "react-native"; -import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import GenreSlide from "@/components/jellyseerr/discover/GenreSlide"; +import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide"; import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; +import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; +import { studios } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; +import { sortBy } from "lodash"; +import type React from "react"; +import { useMemo } from "react"; +import { View } from "react-native"; interface Props { sliders?: DiscoverSlider[]; } const Discover: React.FC = ({ sliders }) => { - if (!sliders) - return; + if (!sliders) return; const sortedSliders = useMemo( - () => sortBy(sliders.filter((s) => s.enabled), 'order', 'asc'), - [sliders] + () => + sortBy( + sliders.filter((s) => s.enabled), + "order", + "asc", + ), + [sliders], ); return ( - - {sortedSliders.map(slide => { + + {sortedSliders.map((slide) => { switch (slide.type) { case DiscoverSliderType.RECENT_REQUESTS: - return + return ( + + ); case DiscoverSliderType.NETWORKS: - return + return ( + + ); case DiscoverSliderType.STUDIOS: - return + return ; case DiscoverSliderType.MOVIE_GENRES: case DiscoverSliderType.TV_GENRES: - return + return ; case DiscoverSliderType.TRENDING: case DiscoverSliderType.POPULAR_MOVIES: case DiscoverSliderType.UPCOMING_MOVIES: case DiscoverSliderType.POPULAR_TV: case DiscoverSliderType.UPCOMING_TV: - return + return ( + + ); } })} - ) + ); }; export default Discover; diff --git a/components/jellyseerr/discover/GenericSlideCard.tsx b/components/jellyseerr/discover/GenericSlideCard.tsx index 776d1424..51292abd 100644 --- a/components/jellyseerr/discover/GenericSlideCard.tsx +++ b/components/jellyseerr/discover/GenericSlideCard.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import {StyleSheet, View, ViewProps} from "react-native"; -import {Image, ImageContentFit} from "expo-image"; -import {Text} from "@/components/common/Text"; -import {LinearGradient} from "expo-linear-gradient"; +import { Text } from "@/components/common/Text"; +import { Image, type ImageContentFit } from "expo-image"; +import { LinearGradient } from "expo-linear-gradient"; +import type React from "react"; +import { StyleSheet, View, type ViewProps } from "react-native"; export const textShadowStyle = StyleSheet.create({ shadow: { @@ -12,48 +12,59 @@ export const textShadowStyle = StyleSheet.create({ height: 1, }, shadowOpacity: 1, - shadowRadius: .5, + shadowRadius: 0.5, elevation: 6, - } -}) + }, +}); -const GenericSlideCard: React.FC<{id: string; url?: string, title?: string, colors?: string[], contentFit?: ImageContentFit} & ViewProps> = ({ +const GenericSlideCard: React.FC< + { + id: string; + url?: string; + title?: string; + colors?: string[]; + contentFit?: ImageContentFit; + } & ViewProps +> = ({ id, url, title, - colors = ['#9333ea', 'transparent'], + colors = ["#9333ea", "transparent"], contentFit = "contain", ...props }) => ( <> - + - {title && - + - {title} - - } + {title} + + + )} ); -export default GenericSlideCard; \ No newline at end of file +export default GenericSlideCard; diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/jellyseerr/discover/GenreSlide.tsx index 3ec7f733..06b10a5a 100644 --- a/components/jellyseerr/discover/GenreSlide.tsx +++ b/components/jellyseerr/discover/GenreSlide.tsx @@ -1,13 +1,14 @@ import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; -import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; -import { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; +import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/constants"; import { useQuery } from "@tanstack/react-query"; import { router, useSegments } from "expo-router"; -import React, { useCallback } from "react"; -import { TouchableOpacity, ViewProps } from "react-native"; +import type React from "react"; +import { useCallback } from "react"; +import { TouchableOpacity, type ViewProps } from "react-native"; const GenreSlide: React.FC = ({ slide, ...props }) => { const segments = useSegments(); @@ -20,7 +21,7 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`, params: { type: slide.type, name: genre.name }, }), - [slide] + [slide], ); const { data, isFetching, isLoading } = useQuery({ @@ -29,7 +30,7 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { return jellyseerrApi?.getGenreSliders( slide.type == DiscoverSliderType.MOVIE_GENRES ? Endpoints.MOVIE - : Endpoints.TV + : Endpoints.TV, ); }, enabled: !!jellyseerrApi, @@ -43,18 +44,18 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { data={data} keyExtractor={(item) => item.id.toString()} renderItem={(item, index) => ( - navigate(item)}> + navigate(item)}> diff --git a/components/jellyseerr/discover/MovieTvSlide.tsx b/components/jellyseerr/discover/MovieTvSlide.tsx index c3d9d690..a5611849 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/jellyseerr/discover/MovieTvSlide.tsx @@ -1,18 +1,25 @@ -import React, {useMemo} from "react"; -import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; +import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { - DiscoverEndpoint, + type DiscoverEndpoint, Endpoints, useJellyseerr, } from "@/hooks/useJellyseerr"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; -import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; -import {ViewProps} from "react-native"; -import {uniqBy} from "lodash"; +import { uniqBy } from "lodash"; +import type React from "react"; +import { useMemo } from "react"; +import type { ViewProps } from "react-native"; -const MovieTvSlide: React.FC = ({ slide, ...props }) => { +const MovieTvSlide: React.FC = ({ + slide, + ...props +}) => { const { jellyseerrApi } = useJellyseerr(); const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -60,10 +67,12 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => const flatData = useMemo( () => uniqBy( - data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), - "id" + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results), + "id", ), - [data] + [data], ); return ( @@ -73,14 +82,16 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => {...props} slide={slide} data={flatData} - keyExtractor={(item) => item!!.id.toString()} + keyExtractor={(item) => item!.id.toString()} onEndReached={() => { - if (hasNextPage) - fetchNextPage() + if (hasNextPage) fetchNextPage(); }} - renderItem={(item) => - - } + renderItem={(item) => ( + + )} /> ) ); diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index dce6d7b9..7f88e40e 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -1,20 +1,28 @@ -import React from "react"; -import {useQuery} from "@tanstack/react-query"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; -import {ViewProps} from "react-native"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common"; +import { useQuery } from "@tanstack/react-query"; +import type React from "react"; +import type { ViewProps } from "react-native"; -const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { - const {jellyseerrApi} = useJellyseerr(); +const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => { + const { jellyseerrApi } = useJellyseerr(); - const { data: details, isLoading, isError } = useQuery({ - queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId], + const { + data: details, + isLoading, + isError, + } = useQuery({ + queryKey: [ + "jellyseerr", + "detail", + request.media.mediaType, + request.media.tmdbId, + ], queryFn: async () => { - return request.media.mediaType == MediaType.MOVIE ? jellyseerrApi?.movieDetails(request.media.tmdbId) : jellyseerrApi?.tvDetails(request.media.tmdbId); @@ -34,34 +42,47 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { }); return ( - - ) -} + + ); +}; -const RecentRequestsSlide: React.FC = ({ slide, ...props }) => { - const {jellyseerrApi} = useJellyseerr(); +const RecentRequestsSlide: React.FC = ({ + slide, + ...props +}) => { + const { jellyseerrApi } = useJellyseerr(); - const { data: requests, isLoading, isError } = useQuery({ + const { + data: requests, + isLoading, + isError, + } = useQuery({ queryKey: ["jellyseerr", "recent_requests"], - queryFn: async () => jellyseerrApi?.requests(), + queryFn: async () => jellyseerrApi?.requests(), enabled: !!jellyseerrApi, refetchOnMount: true, staleTime: 0, }); return ( - requests && requests.results.length > 0 && ( + requests && + requests.results.length > 0 && ( item.id.toString()} renderItem={(item: NonFunctionProperties) => ( - + )} /> ) - ) + ); }; -export default RecentRequestsSlide; \ No newline at end of file +export default RecentRequestsSlide; diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx index 19296fbf..9e2298dc 100644 --- a/components/jellyseerr/discover/Slide.tsx +++ b/components/jellyseerr/discover/Slide.tsx @@ -1,11 +1,12 @@ -import React, {PropsWithChildren} from "react"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { Text } from "@/components/common/Text"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; import { FlashList } from "@shopify/flash-list"; -import {View, ViewProps} from "react-native"; +import type { ContentStyle } from "@shopify/flash-list/src/FlashListProps"; import { t } from "i18next"; -import {ContentStyle} from "@shopify/flash-list/src/FlashListProps"; +import type React from "react"; +import type { PropsWithChildren } from "react"; +import { View, type ViewProps } from "react-native"; export interface SlideProps { slide: DiscoverSlider; @@ -13,17 +14,16 @@ export interface SlideProps { } interface Props extends SlideProps { - data: T[] - renderItem: (item: T, index: number) => - | React.ComponentType - | React.ReactElement - | null - | undefined; + data: T[]; + renderItem: ( + item: T, + index: number, + ) => React.ComponentType | React.ReactElement | null | undefined; keyExtractor: (item: T) => string; onEndReached?: (() => void) | null | undefined; } -const Slide = ({ +const Slide = ({ data, slide, renderItem, @@ -31,18 +31,17 @@ const Slide = ({ onEndReached, contentContainerStyle, ...props -}: PropsWithChildren & ViewProps> -) => { +}: PropsWithChildren & ViewProps>) => { return ( - + {t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())} ({ onEndReachedThreshold={1} onEndReached={onEndReached} //@ts-ignore - renderItem={({item, index}) => item ? renderItem(item, index) : <>} + renderItem={({ item, index }) => + item ? renderItem(item, index) : <> + } /> ); diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index a7b78f0a..34c6f8ef 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -3,7 +3,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { Ionicons } from "@expo/vector-icons"; -import { +import type { BaseItemDto, BaseItemKind, CollectionType, @@ -13,9 +13,9 @@ import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo } from "react"; -import { TouchableOpacityProps, View } from "react-native"; +import { useTranslation } from "react-i18next"; +import { type TouchableOpacityProps, View } from "react-native"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; interface Props extends TouchableOpacityProps { library: BaseItemDto; @@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { api, item: library, }), - [library] + [library], ); const itemType = useMemo(() => { @@ -102,18 +102,18 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { if (settings?.libraryOptions?.display === "row") { return ( - - + + - + {library.Name} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} @@ -124,8 +124,8 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { if (settings?.libraryOptions?.imageStyle === "cover") { return ( - - + + = ({ library, ...props }) => { /> {settings?.libraryOptions?.showTitles && ( - + {library.Name} )} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} @@ -173,21 +173,21 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { return ( - - - + + + {library.Name} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} - + diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index 03f218d1..28752978 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -1,13 +1,13 @@ import { - PropsWithChildren, Children, - isValidElement, + type PropsWithChildren, + type ReactElement, cloneElement, - ReactElement, + isValidElement, } from "react"; -import { StyleSheet, View, ViewProps, ViewStyle } from "react-native"; -import { ListItem } from "./ListItem"; +import { StyleSheet, View, type ViewProps, type ViewStyle } from "react-native"; import { Text } from "../common/Text"; +import { ListItem } from "./ListItem"; interface Props extends ViewProps { title?: string | null | undefined; @@ -24,12 +24,12 @@ export const ListGroup: React.FC> = ({ return ( - + {title} {Children.map(childrenArray, (child, index) => { if (isValidElement<{ style?: ViewStyle }>(child)) { @@ -38,14 +38,14 @@ export const ListGroup: React.FC> = ({ child.props.style, index < childrenArray.length - 1 ? styles.borderBottom - : undefined + : undefined, ), }); } return child; })} - {description && {description}} + {description && {description}} ); }; diff --git a/components/list/ListItem.tsx b/components/list/ListItem.tsx index ea7774a4..892e3b41 100644 --- a/components/list/ListItem.tsx +++ b/components/list/ListItem.tsx @@ -1,10 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; -import { PropsWithChildren, ReactNode } from "react"; +import type { PropsWithChildren, ReactNode } from "react"; import { TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, - ViewProps, + type ViewProps, } from "react-native"; import { Text } from "../common/Text"; @@ -86,10 +86,10 @@ const ListItemContent = ({ }: Props) => { return ( <> - + {icon && ( - - + + )} {title} {value && ( - - + + {value} )} - {children && {children}} + {children && {children}} {showArrow && ( - + )} diff --git a/components/livetv/HourHeader.tsx b/components/livetv/HourHeader.tsx index 99344e43..f412cb9b 100644 --- a/components/livetv/HourHeader.tsx +++ b/components/livetv/HourHeader.tsx @@ -10,7 +10,7 @@ export const HourHeader = ({ height }: { height: number }) => { return ( { }; const HourCell = ({ hour }: { hour: Date }) => ( - - + + {hour.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", diff --git a/components/livetv/LiveTVGuideRow.tsx b/components/livetv/LiveTVGuideRow.tsx index cbb70d19..83a5ada1 100644 --- a/components/livetv/LiveTVGuideRow.tsx +++ b/components/livetv/LiveTVGuideRow.tsx @@ -1,4 +1,4 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useMemo, useRef } from "react"; import { Dimensions, View } from "react-native"; import { Text } from "../common/Text"; @@ -53,7 +53,7 @@ export const LiveTVGuideRow = ({ } return ( - + {programsWithPositions?.map((p) => ( {(() => { return ( @@ -77,11 +77,11 @@ export const LiveTVGuideRow = ({ ? scrollX - p.position : 0, }} - className="px-4 self-start" + className='px-4 self-start' > {p.Name} diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index ff5e60a2..13db9826 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -1,22 +1,22 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { + type QueryFunction, + type QueryKey, + useQuery, +} from "@tanstack/react-query"; import { useAtom } from "jotai"; import { useCallback } from "react"; -import { View, ViewProps } from "react-native"; +import { View, type ViewProps } from "react-native"; +import { ItemCardText } from "../ItemCardText"; import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll"; import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { ItemCardText } from "../ItemCardText"; import MoviePoster from "../posters/MoviePoster"; -import { - type QueryKey, - type QueryFunction, - useQuery, -} from "@tanstack/react-query"; interface Props extends ViewProps { queryKey: QueryKey; @@ -54,14 +54,14 @@ export const MediaListSection: React.FC = ({ return response.data; }, - [api, user?.Id, collection?.Id] + [api, user?.Id, collection?.Id], ); if (!collection) return null; return ( - + {collection.Name} = ({ item, ...props }) => { return ( - + {item?.Name} - {item?.ProductionYear} + {item?.ProductionYear} ); }; diff --git a/components/navigation/TabBarIcon.tsx b/components/navigation/TabBarIcon.tsx index 17c2ebed..0cc6eff6 100644 --- a/components/navigation/TabBarIcon.tsx +++ b/components/navigation/TabBarIcon.tsx @@ -1,8 +1,8 @@ // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ import Ionicons from "@expo/vector-icons/Ionicons"; -import { type IconProps } from "@expo/vector-icons/build/createIconSet"; -import { type ComponentProps } from "react"; +import type { IconProps } from "@expo/vector-icons/build/createIconSet"; +import type { ComponentProps } from "react"; export function TabBarIcon({ style, diff --git a/components/posters/EpisodePoster.tsx b/components/posters/EpisodePoster.tsx index c82464d5..f5c92485 100644 --- a/components/posters/EpisodePoster.tsx +++ b/components/posters/EpisodePoster.tsx @@ -1,11 +1,11 @@ +import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo, useState } from "react"; import { View } from "react-native"; -import { WatchedIndicator } from "@/components/WatchedIndicator"; type MoviePosterProps = { item: BaseItemDto; @@ -25,7 +25,7 @@ export const EpisodePoster: React.FC = ({ }, [item]); const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); const blurhash = useMemo(() => { @@ -34,7 +34,7 @@ export const EpisodePoster: React.FC = ({ }, [item]); return ( - + = ({ : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", width: "100%", @@ -57,7 +57,7 @@ export const EpisodePoster: React.FC = ({ /> {showProgress && progress > 0 && ( - + )} ); diff --git a/components/posters/ItemPoster.tsx b/components/posters/ItemPoster.tsx index 86575ab9..b8605f97 100644 --- a/components/posters/ItemPoster.tsx +++ b/components/posters/ItemPoster.tsx @@ -1,12 +1,12 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { - BaseItemDto, + type BaseItemDto, BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; -import { ItemImage } from "../common/ItemImage"; -import { WatchedIndicator } from "../WatchedIndicator"; import { useState } from "react"; +import { View, type ViewProps } from "react-native"; +import { WatchedIndicator } from "../WatchedIndicator"; +import { ItemImage } from "../common/ItemImage"; interface Props extends ViewProps { item: BaseItemDto; @@ -19,13 +19,13 @@ export const ItemPoster: React.FC = ({ ...props }) => { const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); if (item.Type === "Movie" || item.Type === "Series" || item.Type === "BoxSet") return ( = ({ /> {showProgress && progress > 0 && ( - + )} ); return ( - + ); }; diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 25c16d4f..b25f8974 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -1,23 +1,30 @@ -import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter"; -import {Text} from "@/components/common/Text"; +import { Tag, Tags } from "@/components/GenreTags"; +import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; +import { Text } from "@/components/common/Text"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {Image} from "expo-image"; -import {useMemo} from "react"; -import {View, ViewProps} from "react-native"; -import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {useTranslation} from "react-i18next"; -import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; -import {Colors} from "@/constants/Colors"; -import {Tag, Tags} from "@/components/GenreTags"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import { Colors } from "@/constants/Colors"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; +import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { Image } from "expo-image"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; interface Props extends ViewProps { item?: MovieResult | TvResult | MovieDetails | TvDetails; @@ -36,7 +43,7 @@ const JellyseerrPoster: React.FC = ({ const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr(); const loadingOpacity = useSharedValue(1); const imageOpacity = useSharedValue(0); - const {t} = useTranslation(); + const { t } = useTranslation(); const imageAnimatedStyle = useAnimatedStyle(() => ({ opacity: imageOpacity.value, @@ -48,65 +55,70 @@ const JellyseerrPoster: React.FC = ({ }; const backdropSrc = useMemo( - () => jellyseerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"), - [item, jellyseerrApi, horizontal] + () => + jellyseerrApi?.imageProxy( + item?.backdropPath, + "w1920_and_h800_multi_faces", + ), + [item, jellyseerrApi, horizontal], ); const posterSrc = useMemo( - () => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face",), - [item, jellyseerrApi, horizontal] + () => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"), + [item, jellyseerrApi, horizontal], ); const title = useMemo(() => getTitle(item), [item]); const releaseYear = useMemo(() => getYear(item), [item]); const mediaType = useMemo(() => getMediaType(item), [item]); - const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal]) - const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal]) + const size = useMemo(() => (horizontal ? "h-28" : "w-28"), [horizontal]); + const ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]); const [canRequest] = useJellyseerrCanRequest(item); - const is4k = useMemo( - () => mediaRequest?.is4k === true, - [mediaRequest] - ); + const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]); const downloadItems = useMemo( - () => (is4k ? mediaRequest?.media.downloadStatus4k : mediaRequest?.media.downloadStatus) || [], - [mediaRequest, is4k] - ) + () => + (is4k + ? mediaRequest?.media.downloadStatus4k + : mediaRequest?.media.downloadStatus) || [], + [mediaRequest, is4k], + ); const progress = useMemo(() => { - const [totalSize, sizeLeft] = downloadItems - .reduce((sum: number[], next: DownloadingItem) => - [sum[0] + next.size, sum[1] + next.sizeLeft], - [0, 0] - ); + const [totalSize, sizeLeft] = downloadItems.reduce( + (sum: number[], next: DownloadingItem) => [ + sum[0] + next.size, + sum[1] + next.sizeLeft, + ], + [0, 0], + ); - return (((totalSize - sizeLeft) / totalSize) * 100); - }, - [downloadItems] - ); + return ((totalSize - sizeLeft) / totalSize) * 100; + }, [downloadItems]); - const requestedSeasons: string[] | undefined = useMemo( - () => { - const seasons = mediaRequest?.seasons?.flatMap(s => s.seasonNumber.toString()) || [] - if (seasons.length > 4) { - const [first, second, third, fourth, ...rest] = seasons; - return [first, second, third, fourth, t("home.settings.plugins.jellyseerr.plus_n_more", {n: rest.length })] - } - return seasons - }, - [mediaRequest] - ); + const requestedSeasons: string[] | undefined = useMemo(() => { + const seasons = + mediaRequest?.seasons?.flatMap((s) => s.seasonNumber.toString()) || []; + if (seasons.length > 4) { + const [first, second, third, fourth, ...rest] = seasons; + return [ + first, + second, + third, + fourth, + t("home.settings.plugins.jellyseerr.plus_n_more", { n: rest.length }), + ]; + } + return seasons; + }, [mediaRequest]); - const available = useMemo( - () => { - const status = mediaRequest?.media?.[is4k ? 'status4k' : 'status']; - return status === MediaStatus.AVAILABLE - }, - [mediaRequest, is4k] - ); + const available = useMemo(() => { + const status = mediaRequest?.media?.[is4k ? "status4k" : "status"]; + return status === MediaStatus.AVAILABLE; + }, [mediaRequest, is4k]); return ( = ({ mediaTitle={title} releaseYear={releaseYear} canRequest={canRequest} - posterSrc={posterSrc!!} + posterSrc={posterSrc!} mediaType={mediaType} > - + {mediaRequest && showDownloadInfo && ( <> - + {!available && !Number.isNaN(progress) && ( <> - - + + {progress?.toFixed(0)}% )} {requestedSeasons.length > 0 && ( @@ -172,19 +185,21 @@ const JellyseerrPoster: React.FC = ({ )} - + {title || ""} - {releaseYear || ""} + + {releaseYear || ""} + ); diff --git a/components/posters/MoviePoster.tsx b/components/posters/MoviePoster.tsx index 46776fb7..61fa3b03 100644 --- a/components/posters/MoviePoster.tsx +++ b/components/posters/MoviePoster.tsx @@ -1,7 +1,7 @@ import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo, useState } from "react"; @@ -27,7 +27,7 @@ const MoviePoster: React.FC = ({ }, [item]); const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); const blurhash = useMemo(() => { @@ -36,7 +36,7 @@ const MoviePoster: React.FC = ({ }, [item]); return ( - + = ({ : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", width: "100%", @@ -59,7 +59,7 @@ const MoviePoster: React.FC = ({ /> {showProgress && progress > 0 && ( - + )} ); diff --git a/components/posters/ParentPoster.tsx b/components/posters/ParentPoster.tsx index 70bc6629..65cc4493 100644 --- a/components/posters/ParentPoster.tsx +++ b/components/posters/ParentPoster.tsx @@ -14,13 +14,13 @@ const ParentPoster: React.FC = ({ id }) => { const url = useMemo( () => `${api?.basePath}/Items/${id}/Images/Primary`, - [id] + [id], ); if (!url || !id) return ( = ({ id }) => { ); return ( - + = ({ id }) => { uri: url, }} cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", }} diff --git a/components/posters/Poster.tsx b/components/posters/Poster.tsx index 68799f47..77718ad0 100644 --- a/components/posters/Poster.tsx +++ b/components/posters/Poster.tsx @@ -12,7 +12,7 @@ const Poster: React.FC = ({ id, url, blurhash }) => { if (!id && !url) return ( = ({ id, url, blurhash }) => { ); return ( - + = ({ id, url, blurhash }) => { : null } key={id} - id={id!!} + id={id!} source={ url ? { @@ -39,7 +39,7 @@ const Poster: React.FC = ({ id, url, blurhash }) => { : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", }} diff --git a/components/posters/SeriesPoster.tsx b/components/posters/SeriesPoster.tsx index e551624a..893fae5a 100644 --- a/components/posters/SeriesPoster.tsx +++ b/components/posters/SeriesPoster.tsx @@ -1,11 +1,11 @@ +import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo, useState } from "react"; import { View } from "react-native"; -import { WatchedIndicator } from "@/components/WatchedIndicator"; type MoviePosterProps = { item: BaseItemDto; @@ -32,7 +32,7 @@ const SeriesPoster: React.FC = ({ item }) => { }, [item]); return ( - + = ({ item }) => { : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ height: "100%", width: "100%", diff --git a/components/search/LoadingSkeleton.tsx b/components/search/LoadingSkeleton.tsx index 8ac38ada..9df34938 100644 --- a/components/search/LoadingSkeleton.tsx +++ b/components/search/LoadingSkeleton.tsx @@ -1,11 +1,11 @@ import { View } from "react-native"; -import { Text } from "../common/Text"; import Animated, { useAnimatedStyle, useAnimatedReaction, useSharedValue, withTiming, } from "react-native-reanimated"; +import { Text } from "../common/Text"; interface Props { isLoading: boolean; @@ -28,29 +28,29 @@ export const LoadingSkeleton: React.FC = ({ isLoading }) => { } else { opacity.value = withTiming(0, { duration: 200 }); } - } + }, ); return ( - + {[1, 2, 3].map((s) => ( - - - + + + {[1, 2, 3].map((i) => ( - - - + + + Nisi mollit voluptate amet. - + Lorem ipsum diff --git a/components/search/SearchItemWrapper.tsx b/components/search/SearchItemWrapper.tsx index b24775ab..81d5e4ce 100644 --- a/components/search/SearchItemWrapper.tsx +++ b/components/search/SearchItemWrapper.tsx @@ -1,11 +1,12 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import React, { PropsWithChildren } from "react"; +import type React from "react"; +import type { PropsWithChildren } from "react"; import { Text } from "../common/Text"; -import {FlashList} from "@shopify/flash-list"; type SearchItemWrapperProps = { ids?: string[] | null; @@ -15,12 +16,12 @@ type SearchItemWrapperProps = { onEndReached?: (() => void) | null | undefined; }; -export const SearchItemWrapper = ({ +export const SearchItemWrapper = ({ ids, items, renderItem, header, - onEndReached + onEndReached, }: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -37,25 +38,25 @@ export const SearchItemWrapper = ({ api, userId: user.Id, itemId: id, - }) + }), ); const results = await Promise.all(itemPromises); // Filter out null items return results.filter( - (item) => item !== null + (item) => item !== null, ) as unknown as BaseItemDto[]; }, enabled: !!ids && ids.length > 0 && !!api && !!user?.Id, - staleTime: Infinity, + staleTime: Number.POSITIVE_INFINITY, }); if (!data && (!items || items.length === 0)) return null; return ( <> - {header} + {header} ({ onEndReachedThreshold={1} onEndReached={onEndReached} //@ts-ignore - renderItem={({item, index}) => item ? renderItem(item) : <>} + renderItem={({ item, index }) => (item ? renderItem(item) : <>)} /> ); diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index e774b561..a0948f2d 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -1,18 +1,19 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { router, useSegments } from "expo-router"; import { useAtom } from "jotai"; -import React, { useMemo } from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; -import Poster from "../posters/Poster"; import { itemRouter } from "../common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; +import Poster from "../posters/Poster"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -41,8 +42,10 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { if (!from) return null; return ( - - {t("item_card.cast_and_crew")} + + + {t("item_card.cast_and_crew")} + i.Id.toString()} @@ -55,11 +58,11 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { // @ts-ignore router.push(url); }} - className="flex flex-col w-28" + className='flex flex-col w-28' > - {i.Name} - {i.Role} + {i.Name} + {i.Role} )} /> diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index 16536a6d..e798bb22 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -1,14 +1,14 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { router } from "expo-router"; import { useAtom } from "jotai"; -import React from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; -import Poster from "../posters/Poster"; +import type React from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; -import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -import { useTranslation } from "react-i18next"; +import Poster from "../posters/Poster"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -20,7 +20,9 @@ export const CurrentSeries: React.FC = ({ item, ...props }) => { return ( - {t("item_card.series")} + + {t("item_card.series")} + = ({ item, ...props }) => { router.push(`/series/${item.SeriesId}`)} - className="flex flex-col space-y-2 w-28" + className='flex flex-col space-y-2 w-28' > = ({ item, ...props }) => { return ( - + {item?.Name} - + { router.push( // @ts-ignore - `/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}` + `/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}`, ); }} > - {item?.SeasonName} + {item?.SeasonName} - {"—"} - {`Episode ${item.IndexNumber}`} + {"—"} + {`Episode ${item.IndexNumber}`} - {item?.ProductionYear} + {item?.ProductionYear} ); }; diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index 0d77ac13..db06be59 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -1,30 +1,35 @@ -import { Text } from "@/components/common/Text"; -import React, { useCallback, useMemo, useState } from "react"; -import { Alert, TouchableOpacity, View } from "react-native"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import { FlashList } from "@shopify/flash-list"; -import { orderBy } from "lodash"; import { Tags } from "@/components/GenreTags"; +import { RoundButton } from "@/components/RoundButton"; +import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import { Text } from "@/components/common/Text"; +import { dateOpts } from "@/components/jellyseerr/DetailFacts"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; -import Season from "@/utils/jellyseerr/server/entity/Season"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; -import { Ionicons } from "@expo/vector-icons"; -import { RoundButton } from "@/components/RoundButton"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type Season from "@/utils/jellyseerr/server/entity/Season"; +import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import { TvResult } from "@/utils/jellyseerr/server/models/Search"; -import {QueryObserverResult, RefetchOptions, useQuery} from "@tanstack/react-query"; -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { Ionicons } from "@expo/vector-icons"; +import { FlashList } from "@shopify/flash-list"; +import { + type QueryObserverResult, + type RefetchOptions, + useQuery, +} from "@tanstack/react-query"; import { Image } from "expo-image"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import { Loader } from "../Loader"; import { t } from "i18next"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; -import {dateOpts} from "@/components/jellyseerr/DetailFacts"; +import { orderBy } from "lodash"; +import type React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Alert, TouchableOpacity, View } from "react-native"; +import { Loader } from "../Loader"; const JellyseerrSeasonEpisodes: React.FC<{ details: TvDetails; @@ -54,26 +59,27 @@ const JellyseerrSeasonEpisodes: React.FC<{ }; const RenderItem = ({ item, index }: any) => { - const { jellyseerrApi, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrApi, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const [imageError, setImageError] = useState(false); const upcomingAirDate = useMemo(() => { const airDate = item.airDate; if (airDate) { - let airDateObj = new Date(airDate); + const airDateObj = new Date(airDate); if (new Date() < airDateObj) { - return airDateObj.toLocaleDateString( - `${locale}-${region}`, - dateOpts - ); + return airDateObj.toLocaleDateString(`${locale}-${region}`, dateOpts); } } }, [item]); return ( - - + + {!imageError ? ( <> { uri: jellyseerrApi?.imageProxy(item.stillPath), }} cachePolicy={"memory-disk"} - contentFit="cover" - className="w-full h-full" + contentFit='cover' + className='w-full h-full' onError={(e) => { setImageError(true); }} /> {upcomingAirDate && ( - - - + + + {upcomingAirDate} @@ -100,26 +109,26 @@ const RenderItem = ({ item, index }: any) => { )} ) : ( - + )} - - + + {item.name} - + {`S${item.seasonNumber}:E${item.episodeNumber}`} - + {item.overview} @@ -129,9 +138,13 @@ const RenderItem = ({ item, index }: any) => { const JellyseerrSeasons: React.FC<{ isLoading: boolean; details?: TvDetails; - hasAdvancedRequest?: boolean, + hasAdvancedRequest?: boolean; onAdvancedRequest?: (data: MediaRequestBody) => void; - refetch: (options?: (RefetchOptions | undefined)) => Promise>; + refetch: ( + options?: RefetchOptions | undefined, + ) => Promise< + QueryObserverResult + >; }> = ({ isLoading, details, @@ -147,10 +160,10 @@ const JellyseerrSeasons: React.FC<{ }>(); const seasons = useMemo(() => { const mediaInfoSeasons = details?.mediaInfo?.seasons?.filter( - (s: Season) => s.seasonNumber !== 0 + (s: Season) => s.seasonNumber !== 0, ); const requestedSeasons = details?.mediaInfo?.requests?.flatMap( - (r: MediaRequest) => r.seasons + (r: MediaRequest) => r.seasons, ); return details.seasons?.map((season) => { return { @@ -159,11 +172,11 @@ const JellyseerrSeasons: React.FC<{ // What our library status is mediaInfoSeasons?.find( (mediaSeason: Season) => - mediaSeason.seasonNumber === season.seasonNumber + mediaSeason.seasonNumber === season.seasonNumber, )?.status ?? // What our request status is requestedSeasons?.find( - (s: Season) => s.seasonNumber === season.seasonNumber + (s: Season) => s.seasonNumber === season.seasonNumber, )?.status ?? // Otherwise set it as unknown MediaStatus.UNKNOWN, @@ -173,7 +186,7 @@ const JellyseerrSeasons: React.FC<{ const allSeasonsAvailable = useMemo( () => seasons?.every((season) => season.status === MediaStatus.AVAILABLE), - [seasons] + [seasons], ); const requestAll = useCallback(() => { @@ -184,13 +197,13 @@ const JellyseerrSeasons: React.FC<{ tvdbId: details.externalIds?.tvdbId, seasons: seasons .filter( - (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0 + (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0, ) .map((s) => s.seasonNumber), - } + }; if (hasAdvancedRequest) { - return onAdvancedRequest?.(body) + return onAdvancedRequest?.(body); } requestMedia(details.name, body, refetch); @@ -199,44 +212,53 @@ const JellyseerrSeasons: React.FC<{ const promptRequestAll = useCallback( () => - Alert.alert(t("jellyseerr.confirm"), t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), [ - { - text: t("jellyseerr.cancel"), - style: "cancel", - }, - { - text: t("jellyseerr.yes"), - onPress: requestAll, - }, - ]), - [requestAll] + Alert.alert( + t("jellyseerr.confirm"), + t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), + [ + { + text: t("jellyseerr.cancel"), + style: "cancel", + }, + { + text: t("jellyseerr.yes"), + onPress: requestAll, + }, + ], + ), + [requestAll], ); - const requestSeason = useCallback(async (canRequest: Boolean, seasonNumber: number) => { - if (canRequest) { - const body: MediaRequestBody = { - mediaId: details.id, - mediaType: MediaType.TV, - tvdbId: details.externalIds?.tvdbId, - seasons: [seasonNumber], - } + const requestSeason = useCallback( + async (canRequest: boolean, seasonNumber: number) => { + if (canRequest) { + const body: MediaRequestBody = { + mediaId: details.id, + mediaType: MediaType.TV, + tvdbId: details.externalIds?.tvdbId, + seasons: [seasonNumber], + }; - if (hasAdvancedRequest) { - return onAdvancedRequest?.(body) - } + if (hasAdvancedRequest) { + return onAdvancedRequest?.(body); + } - requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); - } - }, [requestMedia, hasAdvancedRequest, onAdvancedRequest]); + requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); + } + }, + [requestMedia, hasAdvancedRequest, onAdvancedRequest], + ); if (isLoading) return ( - - {t("item_card.seasons")} + + + {t("item_card.seasons")} + {!allSeasonsAvailable && ( - - + + )} @@ -249,19 +271,21 @@ const JellyseerrSeasons: React.FC<{ data={orderBy( details.seasons.filter((s) => s.seasonNumber !== 0), "seasonNumber", - "desc" + "desc", )} ListHeaderComponent={() => ( - - {t("item_card.seasons")} + + + {t("item_card.seasons")} + {!allSeasonsAvailable && ( - - + + )} )} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => } estimatedItemSize={250} renderItem={({ item: season }) => ( <> @@ -272,17 +296,21 @@ const JellyseerrSeasons: React.FC<{ [season.seasonNumber]: !prevState?.[season.seasonNumber], })) } - className="px-4" + className='px-4' > {[0].map(() => { @@ -292,11 +320,13 @@ const JellyseerrSeasons: React.FC<{ return ( requestSeason(canRequest, season.seasonNumber)} + onPress={() => + requestSeason(canRequest, season.seasonNumber) + } className={canRequest ? "bg-gray-700/40" : undefined} mediaStatus={ seasons?.find( - (s) => s.seasonNumber === season.seasonNumber + (s) => s.seasonNumber === season.seasonNumber, )?.status } showRequestIcon={canRequest} diff --git a/components/series/NextItemButton.tsx b/components/series/NextItemButton.tsx index 02520c9a..7631dc5c 100644 --- a/components/series/NextItemButton.tsx +++ b/components/series/NextItemButton.tsx @@ -1,12 +1,12 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Button } from "../Button"; -import { useRouter } from "expo-router"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "expo-router"; +import { useAtom } from "jotai"; import { useMemo } from "react"; +import { Button } from "../Button"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -62,9 +62,9 @@ export const NextItemButton: React.FC = ({ {...props} > {type === "next" ? ( - + ) : ( - + )} ); diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index c76a61c6..e368bd8e 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -1,18 +1,18 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; +import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; import { useAtom } from "jotai"; -import React from "react"; +import type React from "react"; +import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; -import { HorizontalScroll } from "../common/HorrizontalScroll"; -import { Text } from "../common/Text"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; +import { HorizontalScroll } from "../common/HorrizontalScroll"; +import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { FlashList } from "@shopify/flash-list"; -import { useTranslation } from "react-i18next"; export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { const [user] = useAtom(userAtom); @@ -38,15 +38,17 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { if (!items?.length) return ( - - {t("item_card.next_up")} - {t("item_card.no_items_to_display")} + + {t("item_card.next_up")} + {t("item_card.no_items_to_display")} ); return ( - {t("item_card.next_up")} + + {t("item_card.next_up")} + = ({ seriesId }) => { diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx index 25a09c17..21ce2539 100644 --- a/components/series/SeasonDropdown.tsx +++ b/components/series/SeasonDropdown.tsx @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useEffect, useMemo } from "react"; import { Platform, TouchableOpacity, View } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Text } from "../common/Text"; import { t } from "i18next"; +import { Text } from "../common/Text"; type Props = { item: BaseItemDto; @@ -45,12 +45,12 @@ export const SeasonDropdown: React.FC = ({ title: "Name", index: "IndexNumber", }, - [item] + [item], ); const seasonIndex = useMemo( () => state[(item[keys.id] as string) ?? ""], - [state] + [state], ); useEffect(() => { @@ -60,7 +60,7 @@ export const SeasonDropdown: React.FC = ({ if (initialSeasonIndex !== undefined) { // Use the provided initialSeasonIndex if it exists in the seasons const seasonExists = seasons.some( - (season: any) => season[keys.index] === initialSeasonIndex + (season: any) => season[keys.index] === initialSeasonIndex, ); if (seasonExists) { initialIndex = initialSeasonIndex; @@ -77,7 +77,7 @@ export const SeasonDropdown: React.FC = ({ if (initialIndex !== undefined) { const initialSeason = seasons.find( - (season: any) => season[keys.index] === initialIndex + (season: any) => season[keys.index] === initialIndex, ); if (initialSeason) onSelect(initialSeason!); @@ -92,8 +92,8 @@ export const SeasonDropdown: React.FC = ({ return ( - - + + {t("item_card.season")} {seasonIndex} @@ -102,8 +102,8 @@ export const SeasonDropdown: React.FC = ({ = ({ headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items as BaseItemDto[]; @@ -74,7 +74,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ } const previousId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! - 1 + (ep) => ep.IndexNumber === item.IndexNumber! - 1, )?.Id; if (previousId) { queryClient.prefetchQuery({ @@ -90,7 +90,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ } const nextId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! + 1 + (ep) => ep.IndexNumber === item.IndexNumber! + 1, )?.Id; if (nextId) { queryClient.prefetchQuery({ diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index 6851bbbc..1f86ff90 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -1,24 +1,24 @@ +import { + SeasonDropdown, + type SeasonIndexState, +} from "@/components/series/SeasonDropdown"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { runtimeTicksToSeconds } from "@/utils/time"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { DownloadItems, DownloadSingleItem } from "../DownloadItem"; import { Loader } from "../Loader"; -import { Text } from "../common/Text"; -import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { - SeasonDropdown, - SeasonIndexState, -} from "@/components/series/SeasonDropdown"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { PlayedStatus } from "../PlayedStatus"; -import { useTranslation } from "react-i18next"; +import { Text } from "../common/Text"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; type Props = { item: BaseItemDto; @@ -35,7 +35,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const seasonIndex = useMemo( () => seasonIndexState[item.Id ?? ""], - [item, seasonIndexState] + [item, seasonIndexState], ); const { data: seasons } = useQuery({ @@ -54,7 +54,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items; @@ -66,7 +66,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const selectedSeasonId: string | null = useMemo(() => { const season: BaseItemDto = seasons?.find( (s: BaseItemDto) => - s.IndexNumber === seasonIndex || s.Name === seasonIndex + s.IndexNumber === seasonIndex || s.Name === seasonIndex, ); if (!season?.Id) return null; @@ -92,7 +92,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { if (res.data.TotalRecordCount === 0) console.warn( "No episodes found for season with ID ~", - selectedSeasonId + selectedSeasonId, ); return res.data.Items; @@ -102,7 +102,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const queryClient = useQueryClient(); useEffect(() => { - for (let e of episodes || []) { + for (const e of episodes || []) { queryClient.prefetchQuery({ queryKey: ["item", e.Id], queryFn: async () => { @@ -133,7 +133,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { minHeight: 144 * nrOfEpisodes, }} > - + = ({ item, initialSeasonIndex }) => { }} /> {episodes?.length || 0 > 0 ? ( - + ( - + )} DownloadedIconComponent={() => ( - + )} /> ) : null} - + {isFetching ? ( @@ -178,35 +178,35 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { - - + + - - + + {e.Name} - + {`S${e.ParentIndexNumber?.toString()}:E${e.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(e.RunTimeTicks)} - + {e.Overview} @@ -214,8 +214,8 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { )) )} {(episodes?.length || 0) === 0 ? ( - - + + {t("item_card.no_episodes_for_this_season")} diff --git a/components/series/SeriesActions.tsx b/components/series/SeriesActions.tsx index 569f719d..a687ae60 100644 --- a/components/series/SeriesActions.tsx +++ b/components/series/SeriesActions.tsx @@ -1,14 +1,14 @@ -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Ionicons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useCallback, useMemo } from "react"; import { Alert, Linking, TouchableOpacity, View, - ViewProps, + type ViewProps, } from "react-native"; interface Props extends ViewProps { @@ -42,10 +42,10 @@ export const ItemActions = ({ item, ...props }: Props) => { }, [trailerLink]); return ( - + {trailerLink && ( - + )} diff --git a/components/series/SeriesHeader.tsx b/components/series/SeriesHeader.tsx index 78230c89..4c28feb8 100644 --- a/components/series/SeriesHeader.tsx +++ b/components/series/SeriesHeader.tsx @@ -1,8 +1,8 @@ -import { View } from "react-native"; -import { Text } from "../common/Text"; -import { Ratings } from "../Ratings"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useMemo } from "react"; +import { View } from "react-native"; +import { Ratings } from "../Ratings"; +import { Text } from "../common/Text"; import { ItemActions } from "./SeriesActions"; interface Props { @@ -51,14 +51,14 @@ export const SeriesHeader = ({ item }: Props) => { }, [startYear, endYear]); return ( - - {item?.Name} - {yearString} - - + + {item?.Name} + {yearString} + + - {item?.Overview} + {item?.Overview} ); }; diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx index e9445136..ca6d6da8 100644 --- a/components/settings/AppLanguageSelector.tsx +++ b/components/settings/AppLanguageSelector.tsx @@ -1,11 +1,11 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Platform, TouchableOpacity, View, ViewProps } from "react-native"; -import { Text } from "../common/Text"; +import { APP_LANGUAGES } from "@/i18n"; import { useSettings } from "@/utils/atoms/settings"; +import { useTranslation } from "react-i18next"; +import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; +import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import { APP_LANGUAGES } from "@/i18n"; interface Props extends ViewProps {} @@ -22,18 +22,18 @@ export const AppLanguageSelector: React.FC = ({ ...props }) => { - + {APP_LANGUAGES.find( - (l) => l.value === settings?.preferedLanguage + (l) => l.value === settings?.preferedLanguage, )?.label || t("home.settings.languages.system")} = ({ ...props }) => { + {t("home.settings.audio.audio_hint")} } @@ -46,22 +46,22 @@ export const AudioToggles: React.FC = ({ ...props }) => { - - + + {settings?.defaultAudioLanguage?.DisplayName || t("home.settings.audio.none")} { diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx index 1ffe57a1..798fef37 100644 --- a/components/settings/Dashboard.tsx +++ b/components/settings/Dashboard.tsx @@ -1,11 +1,11 @@ +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; import { useSettings } from "@/utils/atoms/settings"; import { useRouter } from "expo-router"; import React from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import { useSessions, useSessionsProps } from "@/hooks/useSessions"; export const Dashboard = () => { const [settings, updateSettings] = useSettings(); @@ -17,7 +17,7 @@ export const Dashboard = () => { if (!settings) return null; return ( - + router.push("/settings/dashboard/sessions")} diff --git a/components/settings/DisabledSetting.tsx b/components/settings/DisabledSetting.tsx index b340fb96..d0d5c33d 100644 --- a/components/settings/DisabledSetting.tsx +++ b/components/settings/DisabledSetting.tsx @@ -1,13 +1,9 @@ -import {View, ViewProps} from "react-native"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; -const DisabledSetting: React.FC<{disabled: boolean, showText?: boolean, text?: string} & ViewProps> = ({ - disabled = false, - showText = true, - text, - children, - ...props -}) => ( +const DisabledSetting: React.FC< + { disabled: boolean; showText?: boolean; text?: string } & ViewProps +> = ({ disabled = false, showText = true, text, children, ...props }) => ( - {disabled && showText && - {text ?? "Currently disabled by admin."} - } + {disabled && showText && ( + + {text ?? "Currently disabled by admin."} + + )} {children} -) +); -export default DisabledSetting; \ No newline at end of file +export default DisabledSetting; diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index 0d1df837..549923de 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -1,17 +1,21 @@ import { Stepper } from "@/components/inputs/Stepper"; import { useDownload } from "@/providers/DownloadProvider"; -import { DownloadMethod, Settings, useSettings } from "@/utils/atoms/settings"; +import { + DownloadMethod, + type Settings, + useSettings, +} from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; import { useQueryClient } from "@tanstack/react-query"; import { useRouter } from "expo-router"; import React, { useMemo } from "react"; import { Platform, Switch, TouchableOpacity } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { useTranslation } from "react-i18next"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function DownloadSettings({ ...props }) { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -25,13 +29,13 @@ export default function DownloadSettings({ ...props }) { pluginSettings?.downloadMethod?.locked === true && pluginSettings?.remuxConcurrentLimit?.locked === true && pluginSettings?.autoDownload.locked === true, - [pluginSettings] + [pluginSettings], ); if (!settings) return null; return ( - + - - + + {settings.downloadMethod === DownloadMethod.Remux ? t("home.settings.downloads.default") : t("home.settings.downloads.optimized")} { updateSettings({ downloadMethod: DownloadMethod.Remux }); setProcesses([]); @@ -76,7 +80,7 @@ export default function DownloadSettings({ ...props }) { { updateSettings({ downloadMethod: DownloadMethod.Optimized }); setProcesses([]); diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 43e6e3d6..71325ccb 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -1,8 +1,8 @@ import { Button } from "@/components/Button"; +import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; -import { Loader } from "@/components/Loader"; import { MediaListSection } from "@/components/medialists/MediaListSection"; import { Colors } from "@/constants/Colors"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; @@ -11,8 +11,8 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; import { Feather, Ionicons } from "@expo/vector-icons"; -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -24,7 +24,7 @@ import { getUserViewsApi, } from "@jellyfin/sdk/lib/utils/api"; import NetInfo from "@react-native-community/netinfo"; -import { QueryFunction, useQuery } from "@tanstack/react-query"; +import { type QueryFunction, useQuery } from "@tanstack/react-query"; import { useNavigation, usePathname, @@ -94,10 +94,10 @@ export const HomeIndex = () => { onPress={() => { router.push("/(auth)/downloads"); }} - className="p-2" + className='p-2' > @@ -108,7 +108,7 @@ export const HomeIndex = () => { useEffect(() => { cleanCacheDirectory().catch((e) => - console.error("Something went wrong cleaning cache directory") + console.error("Something went wrong cleaning cache directory"), ); }, []); @@ -174,14 +174,14 @@ export const HomeIndex = () => { const userViews = useMemo( () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)), - [data, settings?.hiddenLibraries] + [data, settings?.hiddenLibraries], ); const collections = useMemo(() => { const allow = ["movies", "tvshows"]; return ( userViews?.filter( - (c) => c.CollectionType && allow.includes(c.CollectionType) + (c) => c.CollectionType && allow.includes(c.CollectionType), ) || [] ); }, [userViews]); @@ -194,13 +194,13 @@ export const HomeIndex = () => { await invalidateCache(); setLoading(false); }; - + const createCollectionConfig = useCallback( ( title: string, queryKey: string[], includeItemTypes: BaseItemKind[], - parentId: string | undefined + parentId: string | undefined, ): ScrollingCollectionListSection => ({ title, queryKey, @@ -222,7 +222,7 @@ export const HomeIndex = () => { }, type: "ScrollingCollectionList", }), - [api, user?.Id] + [api, user?.Id], ); let sections: Section[] = []; @@ -244,7 +244,7 @@ export const HomeIndex = () => { title || "", queryKey, includeItemTypes, - c.Id + c.Id, ); }); @@ -312,7 +312,7 @@ export const HomeIndex = () => { try { const suggestions = await getSuggestions(api, user.Id); const nextUpPromises = suggestions.map((series) => - getNextUp(api, user.Id, series.Id) + getNextUp(api, user.Id, series.Id), ); const nextUpResults = await Promise.all(nextUpPromises); @@ -376,32 +376,32 @@ export const HomeIndex = () => { if (isConnected === false) { return ( - - {t("home.no_internet")} - + + {t("home.no_internet")} + {t("home.no_internet_message")} - + ) : ( - - + + {t("home.settings.plugins.jellyseerr.jellyseerr_warning")} - + {t("home.settings.plugins.jellyseerr.server_url")} - - + + {t("home.settings.plugins.jellyseerr.server_url_hint")} - + {t("home.settings.plugins.jellyseerr.password")} diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index aad3b1d3..d8516b79 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -1,14 +1,14 @@ import { Text } from "@/components/common/Text"; +import { Colors } from "@/constants/Colors"; import { useHaptic } from "@/hooks/useHaptic"; import { useDownload } from "@/providers/DownloadProvider"; import { useQuery } from "@tanstack/react-query"; import * as FileSystem from "expo-file-system"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { toast } from "sonner-native"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import {Colors} from "@/constants/Colors"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); @@ -44,11 +44,11 @@ export const StorageSettings = () => { return ( - - - {t("home.settings.storage.storage_title")} + + + {t("home.settings.storage.storage_title")} {size && ( - + {t("home.settings.storage.size_used", { used: Number(size.total - size.remaining).bytesToReadable(), total: size.total?.bytesToReadable(), @@ -56,7 +56,7 @@ export const StorageSettings = () => { )} - + {size && ( <> { )} - + {size && ( <> - - - + + + {t("home.settings.storage.app_usage", { usedSpace: calculatePercentage(size.app, size.total), })} - - - + + + {t("home.settings.storage.device_usage", { availableSpace: calculatePercentage( size.total - size.remaining - size.app, - size.total + size.total, ), })} @@ -105,7 +105,7 @@ export const StorageSettings = () => { diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 6bd5e6c2..ff0bfe6e 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,16 +1,16 @@ -import { Platform, TouchableOpacity, View, ViewProps } from "react-native"; +import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Text } from "../common/Text"; -import { useMedia } from "./MediaContext"; -import { Switch } from "react-native-gesture-handler"; -import { ListGroup } from "../list/ListGroup"; -import { ListItem } from "../list/ListItem"; +import Dropdown from "@/components/common/Dropdown"; +import { Stepper } from "@/components/inputs/Stepper"; +import { useSettings } from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; import { useTranslation } from "react-i18next"; -import { useSettings } from "@/utils/atoms/settings"; -import { Stepper } from "@/components/inputs/Stepper"; -import Dropdown from "@/components/common/Dropdown"; +import { Switch } from "react-native-gesture-handler"; +import { Text } from "../common/Text"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; +import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} @@ -46,7 +46,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t("home.settings.subtitles.subtitle_hint")} } @@ -65,15 +65,15 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } titleExtractor={(item) => item?.DisplayName} title={ - - + + {settings?.defaultSubtitleLanguage?.DisplayName || t("home.settings.subtitles.none")} } @@ -100,15 +100,15 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { keyExtractor={String} titleExtractor={(item) => t(subtitleModeKeys[item]) || String(item)} title={ - - + + {t(subtitleModeKeys[settings?.subtitleMode]) || t("home.settings.subtitles.loading")} } diff --git a/components/settings/UserInfo.tsx b/components/settings/UserInfo.tsx index fdd3db44..d910028f 100644 --- a/components/settings/UserInfo.tsx +++ b/components/settings/UserInfo.tsx @@ -1,13 +1,13 @@ -import { View, ViewProps } from "react-native"; -import { Text } from "../common/Text"; -import { ListItem } from "../list/ListItem"; -import { Button } from "../Button"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; -import Constants from "expo-constants"; import Application from "expo-application"; -import { ListGroup } from "../list/ListGroup"; +import Constants from "expo-constants"; +import { useAtom } from "jotai"; import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; +import { Button } from "../Button"; +import { Text } from "../common/Text"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; interface Props extends ViewProps {} @@ -24,10 +24,22 @@ export const UserInfo: React.FC = ({ ...props }) => { return ( - - - - + + + + ); diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx index 2cfeed1d..8e66e0bf 100644 --- a/components/stacks/NestedTabPageStack.tsx +++ b/components/stacks/NestedTabPageStack.tsx @@ -1,6 +1,6 @@ -import { NativeStackNavigationOptions } from "@react-navigation/native-stack"; +import type { ParamListBase, RouteProp } from "@react-navigation/native"; +import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; import { HeaderBackButton } from "../common/HeaderBackButton"; -import { ParamListBase, RouteProp } from "@react-navigation/native"; type ICommonScreenOptions = | NativeStackNavigationOptions diff --git a/components/video-player/controls/AudioSlider.tsx b/components/video-player/controls/AudioSlider.tsx index c0066c2d..0bc02cdd 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -1,10 +1,13 @@ -import React, { useEffect, useRef } from "react"; -import { View, StyleSheet, Platform } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; +import type React from "react"; +import { useEffect, useRef } from "react"; +import { Platform, StyleSheet, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; -const VolumeManager = Platform.isTV ? null : require("react-native-volume-manager"); +import { useSharedValue } from "react-native-reanimated"; +const VolumeManager = Platform.isTV + ? null + : require("react-native-volume-manager"); import { Ionicons } from "@expo/vector-icons"; -import { VolumeResult } from "react-native-volume-manager"; +import type { VolumeResult } from "react-native-volume-manager"; interface AudioSliderProps { setVisibility: (show: boolean) => void; @@ -50,20 +53,22 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }; useEffect(() => { - const volumeListener = VolumeManager.addVolumeListener((result: VolumeResult) => { - volume.value = result.volume * 100; - setVisibility(true); + const volumeListener = VolumeManager.addVolumeListener( + (result: VolumeResult) => { + volume.value = result.volume * 100; + setVisibility(true); - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } - // Set a new timeout to hide the visibility after 2 seconds - timeoutRef.current = setTimeout(() => { - setVisibility(false); - }, 1000); - }); + // Set a new timeout to hide the visibility after 2 seconds + timeoutRef.current = setTimeout(() => { + setVisibility(false); + }, 1000); + }, + ); return () => { volumeListener.remove(); @@ -92,9 +97,9 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }} /> { }} /> = ({ item, close, goToItem }) => { getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then( (res) => { setSeriesItem(res); - } + }, ); } }, [item.SeriesId]); @@ -80,7 +80,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items; }, @@ -90,7 +90,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const selectedSeasonId: string | null = useMemo( () => seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id, - [seasons, seasonIndex] + [seasons, seasonIndex], ); const { data: episodes, isFetching } = useQuery({ @@ -123,7 +123,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const queryClient = useQueryClient(); useEffect(() => { - for (let e of episodes || []) { + for (const e of episodes || []) { queryClient.prefetchQuery({ queryKey: ["item", e.Id], queryFn: async () => { @@ -187,9 +187,9 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { onPress={async () => { close(); }} - className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2' > - + @@ -216,7 +216,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { showPlayButton={_item.Id !== item.Id} /> - + = ({ item, close, goToItem }) => { > {_item.Name} - + {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(_item.RunTimeTicks)} - + {_item.Overview} diff --git a/components/video-player/controls/NextEpisodeCountDownButton.tsx b/components/video-player/controls/NextEpisodeCountDownButton.tsx index 73e3f828..69e5b38b 100644 --- a/components/video-player/controls/NextEpisodeCountDownButton.tsx +++ b/components/video-player/controls/NextEpisodeCountDownButton.tsx @@ -1,6 +1,13 @@ -import React, { useEffect } from "react"; -import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { Text } from "@/components/common/Text"; +import { Colors } from "@/constants/Colors"; +import type React from "react"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -8,8 +15,6 @@ import Animated, { Easing, runOnJS, } from "react-native-reanimated"; -import { Colors } from "@/constants/Colors"; -import { useTranslation } from "react-i18next"; interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps { onFinish?: () => void; @@ -38,7 +43,7 @@ const NextEpisodeCountDownButton: React.FC = ({ if (finished && onFinish) { runOnJS(onFinish)(); } - } + }, ); } }, [show, onFinish]); @@ -68,13 +73,15 @@ const NextEpisodeCountDownButton: React.FC = ({ return ( - - {t("player.next_episode")} + + + {t("player.next_episode")} + ); diff --git a/components/video-player/controls/SkipButton.tsx b/components/video-player/controls/SkipButton.tsx index 15bd5fa6..016f94d1 100644 --- a/components/video-player/controls/SkipButton.tsx +++ b/components/video-player/controls/SkipButton.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { View, TouchableOpacity, Text, ViewProps } from "react-native"; +import type React from "react"; +import { Text, TouchableOpacity, View, type ViewProps } from "react-native"; interface SkipButtonProps extends ViewProps { onPress: () => void; @@ -17,9 +17,9 @@ const SkipButton: React.FC = ({ - {buttonText} + {buttonText} ); diff --git a/components/video-player/controls/SliderScrubbter.tsx b/components/video-player/controls/SliderScrubbter.tsx index a618a350..2259648e 100644 --- a/components/video-player/controls/SliderScrubbter.tsx +++ b/components/video-player/controls/SliderScrubbter.tsx @@ -1,11 +1,12 @@ -import { useTrickplay } from '@/hooks/useTrickplay'; -import { formatTimeString, msToTicks, ticksToSeconds } from '@/utils/time'; -import React, { useRef, useState } from 'react'; -import { View, Text } from 'react-native'; +import { useTrickplay } from "@/hooks/useTrickplay"; +import { formatTimeString, msToTicks, ticksToSeconds } from "@/utils/time"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; +import type React from "react"; +import { useRef, useState } from "react"; +import { Text, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; -import { SharedValue, useSharedValue } from 'react-native-reanimated'; -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { type SharedValue, useSharedValue } from "react-native-reanimated"; interface SliderScrubberProps { cacheProgress: SharedValue; @@ -30,12 +31,9 @@ const SliderScrubber: React.FC = ({ remainingTime, item, }) => { - - const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 }); - const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( - item, - ); + const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = + useTrickplay(item); const handleSliderChange = (value: number) => { const progressInTicks = msToTicks(value); @@ -86,7 +84,7 @@ const SliderScrubber: React.FC = ({ marginTop: -tileHeight / 4 - 60, zIndex: 10, }} - className=" bg-neutral-800 overflow-hidden" + className=' bg-neutral-800 overflow-hidden' > = ({ ], }} source={{ uri: url }} - contentFit="cover" + contentFit='cover' /> = ({ > {`${time.hours > 0 ? `${time.hours}:` : ""}${ time.minutes < 10 ? `0${time.minutes}` : time.minutes - }:${ - time.seconds < 10 ? `0${time.seconds}` : time.seconds - }`} + }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`} ); @@ -129,11 +125,11 @@ const SliderScrubber: React.FC = ({ minimumValue={min} maximumValue={max} /> - - + + {formatTimeString(currentTime, "ms")} - + -{formatTimeString(remainingTime, "ms")} @@ -141,4 +137,4 @@ const SliderScrubber: React.FC = ({ ); }; -export default SliderScrubber; \ No newline at end of file +export default SliderScrubber; diff --git a/components/video-player/controls/contexts/ControlContext.tsx b/components/video-player/controls/contexts/ControlContext.tsx index 30e9d50b..3eed62bd 100644 --- a/components/video-player/controls/contexts/ControlContext.tsx +++ b/components/video-player/controls/contexts/ControlContext.tsx @@ -1,8 +1,9 @@ -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import React, { createContext, useContext, useState, ReactNode } from "react"; +import type React from "react"; +import { type ReactNode, createContext, useContext, useState } from "react"; interface ControlContextProps { item: BaseItemDto; @@ -11,7 +12,7 @@ interface ControlContextProps { } const ControlContext = createContext( - undefined + undefined, ); interface ControlProviderProps { diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index 094b7b04..0ef73f31 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -1,8 +1,16 @@ -import { TrackInfo } from "@/modules/VlcPlayer.types"; -import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react"; -import { useControlContext } from "./ControlContext"; -import { Track } from "../types"; +import type { TrackInfo } from "@/modules/VlcPlayer.types"; import { router, useLocalSearchParams } from "expo-router"; +import type React from "react"; +import { + type ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import type { Track } from "../types"; +import { useControlContext } from "./ControlContext"; interface VideoContextProps { audioTracks: Track[] | null; @@ -16,8 +24,14 @@ const VideoContext = createContext(undefined); interface VideoProviderProps { children: ReactNode; - getAudioTracks: (() => Promise) | (() => TrackInfo[]) | undefined; - getSubtitleTracks: (() => Promise) | (() => TrackInfo[]) | undefined; + getAudioTracks: + | (() => Promise) + | (() => TrackInfo[]) + | undefined; + getSubtitleTracks: + | (() => Promise) + | (() => TrackInfo[]) + | undefined; setAudioTrack: ((index: number) => void) | undefined; setSubtitleTrack: ((index: number) => void) | undefined; setSubtitleURL: ((url: string, customName: string) => void) | undefined; @@ -38,20 +52,24 @@ export const VideoProvider: React.FC = ({ const isVideoLoaded = ControlContext?.isVideoLoaded; const mediaSource = ControlContext?.mediaSource; - const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; + const allSubs = + mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; - const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); + const { itemId, audioIndex, bitrateValue, subtitleIndex } = + useLocalSearchParams<{ + itemId: string; + audioIndex: string; + subtitleIndex: string; + mediaSourceId: string; + bitrateValue: string; + }>(); const onTextBasedSubtitle = useMemo( () => - allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1", - [allSubs, subtitleIndex] + allSubs.find( + (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream, + ) || subtitleIndex === "-1", + [allSubs, subtitleIndex], ); const setPlayerParams = ({ @@ -74,14 +92,21 @@ export const VideoProvider: React.FC = ({ router.replace(`player/direct-player?${queryParams}`); }; - const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => { + const setTrackParams = ( + type: "audio" | "subtitle", + index: number, + serverIndex: number, + ) => { const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack; const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex"; // If we're transcoding and we're going from a image based subtitle // to a text based subtitle, we need to change the player params. - const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle; + const shouldChangePlayerParams = + type === "subtitle" && + mediaSource?.TranscodingUrl && + !onTextBasedSubtitle; console.log("Set player params", index, serverIndex); if (shouldChangePlayerParams) { @@ -102,16 +127,19 @@ export const VideoProvider: React.FC = ({ const subtitleData = await getSubtitleTracks(); // Step 1: Move external subs to the end, because VLC puts external subs at the end - const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)); + const sortedSubs = allSubs.sort( + (a, b) => Number(a.IsExternal) - Number(b.IsExternal), + ); // Step 2: Apply VLC indexing logic let textSubIndex = 0; const processedSubs: Track[] = sortedSubs?.map((sub) => { // Always increment for non-transcoding subtitles // Only increment for text-based subtitles when transcoding - const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; + const shouldIncrement = + !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1; - const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1; + const finalIndex = shouldIncrement ? vlcIndex : (sub.Index ?? -1); if (shouldIncrement) textSubIndex++; return { @@ -127,7 +155,9 @@ export const VideoProvider: React.FC = ({ }); // Step 3: Restore the original order - const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index); + const subtitles: Track[] = processedSubs.sort( + (a, b) => a.index - b.index, + ); // Add a "Disable Subtitles" option subtitles.unshift({ @@ -143,20 +173,23 @@ export const VideoProvider: React.FC = ({ if (getAudioTracks) { const audioData = await getAudioTracks(); - const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; + const allAudio = + mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; const audioTracks: Track[] = allAudio?.map((audio, idx) => { if (!mediaSource?.TranscodingUrl) { const vlcIndex = audioData?.at(idx)?.index ?? -1; return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1), + setTrack: () => + setTrackParams("audio", vlcIndex, audio.Index ?? -1), }; } return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), + setTrack: () => + setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), }; }); setAudioTracks(audioTracks); diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index ed329659..3168e942 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,17 +1,20 @@ -import React, { useCallback } from "react"; -import { TouchableOpacity, Platform } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import React, { useCallback } from "react"; +import { Platform, TouchableOpacity } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { useVideoContext } from "../contexts/VideoContext"; -import { useLocalSearchParams, useRouter } from "expo-router"; import { BITRATES } from "@/components/BitrateSelector"; +import { useLocalSearchParams, useRouter } from "expo-router"; import { useControlContext } from "../contexts/ControlContext"; +import { useVideoContext } from "../contexts/VideoContext"; const DropdownView = () => { const videoContext = useVideoContext(); const { subtitleTracks, audioTracks } = videoContext; const ControlContext = useControlContext(); - const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource]; + const [item, mediaSource] = [ + ControlContext?.item, + ControlContext?.mediaSource, + ]; const router = useRouter(); const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{ @@ -34,27 +37,29 @@ const DropdownView = () => { // @ts-expect-error router.replace(`player/direct-player?${queryParams}`); }, - [item, mediaSource, subtitleIndex, audioIndex] + [item, mediaSource, subtitleIndex, audioIndex], ); return ( - - + + - Quality + + Quality + { changeBitrate(bitrate.value?.toString() ?? "")} + onValueChange={() => + changeBitrate(bitrate.value?.toString() ?? "") + } > - {bitrate.key} + + {bitrate.key} + ))} - Subtitle + + Subtitle + { value={subtitleIndex === sub.index.toString()} onValueChange={() => sub.setTrack()} > - {sub.name} + + {sub.name} + ))} - Audio + + Audio + { value={audioIndex === track.index.toString()} onValueChange={() => track.setTrack()} > - {track.name} + + {track.name} + ))} diff --git a/components/video-player/controls/types.ts b/components/video-player/controls/types.ts index 8040f6d3..f6c0e00a 100644 --- a/components/video-player/controls/types.ts +++ b/components/video-player/controls/types.ts @@ -23,4 +23,4 @@ type Track = { setTrack: () => void; }; -export { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track }; +export type { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track }; diff --git a/components/video-player/controls/useTapDetection.tsx b/components/video-player/controls/useTapDetection.tsx index 041e6d39..545e1c2c 100644 --- a/components/video-player/controls/useTapDetection.tsx +++ b/components/video-player/controls/useTapDetection.tsx @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { GestureResponderEvent } from "react-native"; +import type { GestureResponderEvent } from "react-native"; interface TapDetectionOptions { maxDuration?: number; @@ -33,7 +33,7 @@ export const useTapDetection = ({ const touchDuration = touchEndTime - touchStartTime.current; const touchDistance = Math.sqrt( Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) + - Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2) + Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2), ); if (touchDuration < maxDuration && touchDistance < maxDistance) { diff --git a/components/vlc/VideoDebugInfo.tsx b/components/vlc/VideoDebugInfo.tsx index 0d2fb0df..d936cb90 100644 --- a/components/vlc/VideoDebugInfo.tsx +++ b/components/vlc/VideoDebugInfo.tsx @@ -1,12 +1,10 @@ -import { - TrackInfo, - VlcPlayerViewRef, -} from "@/modules/VlcPlayer.types"; -import React, { useEffect, useState } from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "../common/Text"; -import { useTranslation } from "react-i18next"; interface Props extends ViewProps { playerRef: React.RefObject; @@ -15,7 +13,7 @@ interface Props extends ViewProps { export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { const [audioTracks, setAudioTracks] = useState(null); const [subtitleTracks, setSubtitleTracks] = useState( - null + null, ); useEffect(() => { @@ -45,15 +43,15 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { }} {...props} > - {t("player.playback_state")} - {t("player.audio_tracks")} + {t("player.playback_state")} + {t("player.audio_tracks")} {audioTracks && audioTracks.map((track, index) => ( {track.name} ({t("player.index")} {track.index}) ))} - {t("player.subtitles_tracks")} + {t("player.subtitles_tracks")} {subtitleTracks && subtitleTracks.map((track, index) => ( @@ -61,7 +59,7 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { ))} { if (playerRef.current) { playerRef.current.getAudioTracks().then(setAudioTracks); @@ -69,7 +67,9 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { } }} > - {t("player.refresh_tracks")} + + {t("player.refresh_tracks")} + ); diff --git a/constants/Languages.ts b/constants/Languages.ts index 0a6d63b1..8014e380 100644 --- a/constants/Languages.ts +++ b/constants/Languages.ts @@ -1,4 +1,4 @@ -import { DefaultLanguageOption } from "@/utils/atoms/settings"; +import type { DefaultLanguageOption } from "@/utils/atoms/settings"; export const LANGUAGES: DefaultLanguageOption[] = [ { label: "English", value: "eng" }, diff --git a/hooks/useAdjacentEpisodes.ts b/hooks/useAdjacentEpisodes.ts index 6fb32ac3..4c5a6e2b 100644 --- a/hooks/useAdjacentEpisodes.ts +++ b/hooks/useAdjacentEpisodes.ts @@ -1,9 +1,9 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; import { useAtomValue } from "jotai"; +import { useMemo } from "react"; interface AdjacentEpisodesProps { item?: BaseItemDto | null; diff --git a/hooks/useControlsVisibility.ts b/hooks/useControlsVisibility.ts index 964c296a..71c6197d 100644 --- a/hooks/useControlsVisibility.ts +++ b/hooks/useControlsVisibility.ts @@ -5,11 +5,11 @@ import { useSharedValue, } from "react-native-reanimated"; -export const useControlsVisibility = (timeout: number = 3000) => { +export const useControlsVisibility = (timeout = 3000) => { const opacity = useSharedValue(1); const hideControlsTimerRef = useRef | null>( - null + null, ); const showControls = useCallback(() => { diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index 14a77161..0317f66b 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; import { useHaptic } from "./useHaptic"; interface CreditTimestamps { @@ -25,7 +25,7 @@ export const useCreditSkipper = ( currentTime: number, seek: (time: number) => void, play: () => void, - isVlc: boolean = false + isVlc = false, ) => { const [api] = useAtom(apiAtom); const [showSkipCreditButton, setShowSkipCreditButton] = useState(false); @@ -54,7 +54,7 @@ export const useCreditSkipper = ( `${api.basePath}/Episode/${itemId}/Timestamps`, { headers: getAuthHeaders(api), - } + }, ); if (res?.status !== 200) { @@ -71,7 +71,7 @@ export const useCreditSkipper = ( if (creditTimestamps) { setShowSkipCreditButton( currentTime > creditTimestamps.Credits.Start && - currentTime < creditTimestamps.Credits.End + currentTime < creditTimestamps.Credits.End, ); } }, [creditTimestamps, currentTime]); diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts index 9e0424fe..0991def0 100644 --- a/hooks/useDefaultPlaySettings.ts +++ b/hooks/useDefaultPlaySettings.ts @@ -1,7 +1,7 @@ -import { Bitrate, BITRATES } from "@/components/BitrateSelector"; -import { Settings } from "@/utils/atoms/settings"; +import { BITRATES, Bitrate } from "@/components/BitrateSelector"; +import type { Settings } from "@/utils/atoms/settings"; import { - BaseItemDto, + type BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import { useMemo } from "react"; @@ -9,7 +9,7 @@ import { useMemo } from "react"; // Used only for initial play settings. const useDefaultPlaySettings = ( item: BaseItemDto, - settings: Settings | null + settings: Settings | null, ) => { const playSettings = useMemo(() => { // 1. Get first media source @@ -21,11 +21,11 @@ const useDefaultPlaySettings = ( (x) => x.Type === "Audio" && x.Language === - settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName + settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName, )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" + (x) => x.Type === "Audio", )?.Index; // 4. Get default bitrate from settings or fallback to max diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts index 4c630710..91cf1fbe 100644 --- a/hooks/useDownloadedFileOpener.ts +++ b/hooks/useDownloadedFileOpener.ts @@ -1,6 +1,6 @@ import { usePlaySettings } from "@/providers/PlaySettingsProvider"; import { writeToLog } from "@/utils/log"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; import { useCallback } from "react"; @@ -41,7 +41,7 @@ export const useDownloadedFileOpener = () => { console.error("Error opening file:", error); } }, - [setOfflineSettings, setPlayUrl, router] + [setOfflineSettings, setPlayUrl, router], ); return { openFile }; diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index 437d290e..74a0216e 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,9 +1,9 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; export const useFavorite = (item: BaseItemDto) => { const queryClient = useQueryClient(); @@ -26,7 +26,7 @@ export const useFavorite = (item: BaseItemDto) => { ...newData, UserData: { ...old.UserData, ...newData.UserData }, }; - } + }, ); }; diff --git a/hooks/useHaptic.ts b/hooks/useHaptic.ts index 51b26f83..132a599c 100644 --- a/hooks/useHaptic.ts +++ b/hooks/useHaptic.ts @@ -1,6 +1,6 @@ +import { useSettings } from "@/utils/atoms/settings"; import { useCallback, useMemo } from "react"; import { Platform } from "react-native"; -import { useSettings } from "@/utils/atoms/settings"; const Haptics = !Platform.isTV ? require("expo-haptics") : null; export type HapticFeedbackType = @@ -25,7 +25,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : () => Haptics.impactAsync(type); }, - [] + [], ); const createNotificationFeedback = useCallback( (type: typeof Haptics.NotificationFeedbackType) => { @@ -33,7 +33,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : () => Haptics.notificationAsync(type); }, - [] + [], ); const hapticHandlers = useMemo( @@ -46,14 +46,14 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : Haptics.selectionAsync, success: createNotificationFeedback( - Haptics.NotificationFeedbackType.Success + Haptics.NotificationFeedbackType.Success, ), warning: createNotificationFeedback( - Haptics.NotificationFeedbackType.Warning + Haptics.NotificationFeedbackType.Warning, ), error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error), }), - [createHapticHandler, createNotificationFeedback] + [createHapticHandler, createNotificationFeedback], ); if (settings?.disableHapticFeedback) { diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts index 4928ccc7..5f67f42f 100644 --- a/hooks/useImageColors.ts +++ b/hooks/useImageColors.ts @@ -7,7 +7,7 @@ import { } from "@/utils/atoms/primaryColor"; import { getItemImage } from "@/utils/getItemImage"; import { storage } from "@/utils/mmkv"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtom, useAtomValue } from "jotai"; import { useEffect, useMemo } from "react"; import { Platform } from "react-native"; @@ -70,40 +70,48 @@ export const useImageColors = ({ fallback: "#fff", cache: false, }) - .then((colors: { platform: string; dominant: string; vibrant: string; detail: string; primary: string; }) => { - let primary: string = "#fff"; - let text: string = "#000"; - let backup: string = "#fff"; + .then( + (colors: { + platform: string; + dominant: string; + vibrant: string; + detail: string; + primary: string; + }) => { + let primary = "#fff"; + let text = "#000"; + let backup = "#fff"; - // Select the appropriate color based on the platform - if (colors.platform === "android") { - primary = colors.dominant; - backup = colors.vibrant; - } else if (colors.platform === "ios") { - primary = colors.detail; - backup = colors.primary; - } + // Select the appropriate color based on the platform + if (colors.platform === "android") { + primary = colors.dominant; + backup = colors.vibrant; + } else if (colors.platform === "ios") { + primary = colors.detail; + backup = colors.primary; + } - // Adjust the primary color if it's too close to black - if (primary && isCloseToBlack(primary)) { - if (backup && !isCloseToBlack(backup)) primary = backup; - primary = adjustToNearBlack(primary); - } + // Adjust the primary color if it's too close to black + if (primary && isCloseToBlack(primary)) { + if (backup && !isCloseToBlack(backup)) primary = backup; + primary = adjustToNearBlack(primary); + } - // Calculate the text color based on the primary color - if (primary) text = calculateTextColor(primary); + // Calculate the text color based on the primary color + if (primary) text = calculateTextColor(primary); - setPrimaryColor({ - primary, - text, - }); + setPrimaryColor({ + primary, + text, + }); - // Cache the colors in storage - if (source.uri && primary) { - storage.set(`${source.uri}-primary`, primary); - storage.set(`${source.uri}-text`, text); - } - }) + // Cache the colors in storage + if (source.uri && primary) { + storage.set(`${source.uri}-primary`, primary); + storage.set(`${source.uri}-text`, text); + } + }, + ) .catch((error: any) => { console.error("Error getting colors", error); }); diff --git a/hooks/useImageStorage.ts b/hooks/useImageStorage.ts index f379de1c..1c0bc362 100644 --- a/hooks/useImageStorage.ts +++ b/hooks/useImageStorage.ts @@ -62,7 +62,7 @@ const useImageStorage = () => { console.warn("Error saving image:", error); } }, - [] + [], ); const loadImage = useCallback(async (key: string) => { diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index b41872dc..ab38148c 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; import { useHaptic } from "./useHaptic"; interface IntroTimestamps { @@ -26,7 +26,7 @@ export const useIntroSkipper = ( currentTime: number, seek: (ticks: number) => void, play: () => void, - isVlc: boolean = false + isVlc = false, ) => { const [api] = useAtom(apiAtom); const [showSkipButton, setShowSkipButton] = useState(false); @@ -54,7 +54,7 @@ export const useIntroSkipper = ( `${api.basePath}/Episode/${itemId}/IntroTimestamps`, { headers: getAuthHeaders(api), - } + }, ); if (res?.status !== 200) { @@ -71,7 +71,7 @@ export const useIntroSkipper = ( if (introTimestamps) { setShowSkipButton( currentTime > introTimestamps.ShowSkipPromptAt && - currentTime < introTimestamps.HideSkipPromptAt + currentTime < introTimestamps.HideSkipPromptAt, ); } }, [introTimestamps, currentTime]); diff --git a/hooks/useJellyfinDiscovery.tsx b/hooks/useJellyfinDiscovery.tsx index 963dfe81..3bb0757a 100644 --- a/hooks/useJellyfinDiscovery.tsx +++ b/hooks/useJellyfinDiscovery.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useCallback, useState } from "react"; import dgram from "react-native-udp"; const JELLYFIN_DISCOVERY_PORT = 7359; @@ -53,7 +53,7 @@ export const useJellyfinDiscovery = () => { return; } console.log("Discovery message sent successfully"); - } + }, ); discoveryTimeout = setTimeout(() => { diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index f7400dc1..920d3404 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,45 +1,52 @@ -import axios, { AxiosError, AxiosInstance } from "axios"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import type { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; +import type { + MovieResult, + Results, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { storage } from "@/utils/mmkv"; -import { inRange } from "lodash"; -import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; +import axios, { type AxiosError, type AxiosInstance } from "axios"; import { atom } from "jotai"; import { useAtom } from "jotai/index"; +import { inRange } from "lodash"; import "@/augmentations"; -import { useCallback, useMemo } from "react"; import { useSettings } from "@/utils/atoms/settings"; -import { toast } from "sonner-native"; +import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; +import { + IssueStatus, + type IssueType, +} from "@/utils/jellyseerr/server/constants/issue"; import { MediaRequestStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { - SeasonWithEpisodes, - TvDetails, -} from "@/utils/jellyseerr/server/models/Tv"; -import { - IssueStatus, - IssueType, -} from "@/utils/jellyseerr/server/constants/issue"; -import Issue from "@/utils/jellyseerr/server/entity/Issue"; -import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; -import { writeErrorLog } from "@/utils/log"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import { t } from "i18next"; -import { +import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; +import type Issue from "@/utils/jellyseerr/server/entity/Issue"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; +import type { + MediaRequestBody, + RequestResultsResponse, +} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import type { + ServiceCommonServer, + ServiceCommonServerWithDetails, +} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces"; +import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/api/userInterfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { CombinedCredit, PersonDetails, } from "@/utils/jellyseerr/server/models/Person"; +import type { + SeasonWithEpisodes, + TvDetails, +} from "@/utils/jellyseerr/server/models/Tv"; +import { writeErrorLog } from "@/utils/log"; import { useQueryClient } from "@tanstack/react-query"; -import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; -import {UserResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/userInterfaces"; -import { - ServiceCommonServer, - ServiceCommonServerWithDetails -} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces"; +import { t } from "i18next"; +import { useCallback, useMemo } from "react"; +import { toast } from "sonner-native"; interface SearchParams { query: string; @@ -134,15 +141,16 @@ export class JellyseerrApi { const { status, headers, data } = response; if (inRange(status, 200, 299)) { if (data.version < "2.0.0") { - const error = - t("jellyseerr.toasts.jellyseer_does_not_meet_requirements"); + const error = t( + "jellyseerr.toasts.jellyseer_does_not_meet_requirements", + ); toast.error(error); throw Error(error); } storage.setAny( JELLYSEERR_COOKIES, - headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [] + headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [], ); return { isValid: true, @@ -154,7 +162,7 @@ export class JellyseerrApi { `Jellyseerr returned a ${status} for url:\n` + response.config.url + "\n" + - JSON.stringify(response.data) + JSON.stringify(response.data), ); return { isValid: false, @@ -190,14 +198,14 @@ export class JellyseerrApi { async discoverSettings(): Promise { return this.axios ?.get( - Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER + Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER, ) .then(({ data }) => data); } async discover( endpoint: DiscoverEndpoint | string, - params: any + params: any, ): Promise { return this.axios ?.get(Endpoints.API_V1 + endpoint, { params }) @@ -206,18 +214,23 @@ export class JellyseerrApi { async getGenreSliders( endpoint: Endpoints.TV | Endpoints.MOVIE, - params: any = undefined + params: any = undefined, ): Promise { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.DISCOVER + Endpoints.GENRE_SLIDER + endpoint, { params }) + ?.get( + Endpoints.API_V1 + + Endpoints.DISCOVER + + Endpoints.GENRE_SLIDER + + endpoint, + { params }, + ) .then(({ data }) => data); } async search(params: SearchParams): Promise { - return this.axios?.get( - Endpoints.API_V1 + Endpoints.SEARCH, - { params } - ).then(({ data }) => data) + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.SEARCH, { params }) + .then(({ data }) => data); } async request(request: MediaRequestBody): Promise { @@ -232,15 +245,19 @@ export class JellyseerrApi { .then(({ data }) => data); } - async requests(params = { - filter: "all", - take: 10, - sort: "modified", - skip: 0 - }): Promise { + async requests( + params = { + filter: "all", + take: 10, + sort: "modified", + skip: 0, + }, + ): Promise { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.REQUEST, {params}) - .then(({data}) => data); + ?.get(Endpoints.API_V1 + Endpoints.REQUEST, { + params, + }) + .then(({ data }) => data); } async movieDetails(id: number) { @@ -265,7 +282,7 @@ export class JellyseerrApi { Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + - Endpoints.COMBINED_CREDITS + Endpoints.COMBINED_CREDITS, ) .then((response) => { return response?.data; @@ -275,7 +292,7 @@ export class JellyseerrApi { async movieRatings(id: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}` + `${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}`, ) .then(({ data }) => data); } @@ -291,7 +308,7 @@ export class JellyseerrApi { async tvRatings(id: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}` + `${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}`, ) .then(({ data }) => data); } @@ -299,7 +316,7 @@ export class JellyseerrApi { async tvSeason(id: number, seasonId: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}` + `${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}`, ) .then((response) => { return response?.data; @@ -308,21 +325,18 @@ export class JellyseerrApi { async user(params: any) { return this.axios - ?.get(`${Endpoints.API_V1}${Endpoints.USER}`, { params }) - .then(({data}) => data.results) + ?.get(`${Endpoints.API_V1}${Endpoints.USER}`, { + params, + }) + .then(({ data }) => data.results); } - imageProxy( - path?: string, - filter: string = "original", - width: number = 1920, - quality: number = 75 - ) { + imageProxy(path?: string, filter = "original", width = 1920, quality = 75) { return path ? this.axios.defaults.baseURL + `/_next/image?` + new URLSearchParams( - `url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}` + `url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`, ).toString() : this.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`; @@ -345,16 +359,20 @@ export class JellyseerrApi { }); } - async service(type: 'radarr' | 'sonarr') { + async service(type: "radarr" | "sonarr") { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`) - .then(({data}) => data); + ?.get( + Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`, + ) + .then(({ data }) => data); } - async serviceDetails(type: 'radarr' | 'sonarr', id: number) { + async serviceDetails(type: "radarr" | "sonarr", id: number) { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`) - .then(({data}) => data); + ?.get( + Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`, + ) + .then(({ data }) => data); } private setInterceptors() { @@ -364,7 +382,7 @@ export class JellyseerrApi { if (cookies) { storage.setAny( JELLYSEERR_COOKIES, - response.headers["set-cookie"]?.flatMap((c) => c.split("; ")) + response.headers["set-cookie"]?.flatMap((c) => c.split("; ")), ); } return response; @@ -378,20 +396,20 @@ export class JellyseerrApi { `error: ${error.toString()}\n` + `url: ${error?.config?.url}\n` + `data:\n` + - JSON.stringify(error.response?.data) + JSON.stringify(error.response?.data), ); if (error.status === 403) { clearJellyseerrStorageData(); } return Promise.reject(error); - } + }, ); this.axios.interceptors.request.use( async (config) => { const cookies = storage.get(JELLYSEERR_COOKIES); if (cookies) { - const headerName = this.axios.defaults.xsrfHeaderName!!; + const headerName = this.axios.defaults.xsrfHeaderName!; const xsrfToken = cookies .find((c) => c.includes(headerName)) ?.split(headerName + "=")?.[1]; @@ -403,7 +421,7 @@ export class JellyseerrApi { }, (error) => { console.error("Jellyseerr request error", error); - } + }, ); } } @@ -439,55 +457,74 @@ export const useJellyseerr = () => { switch (mediaRequest.status) { case MediaRequestStatus.PENDING: case MediaRequestStatus.APPROVED: - toast.success(t("jellyseerr.toasts.requested_item", {item: title})); - onSuccess?.() + toast.success( + t("jellyseerr.toasts.requested_item", { item: title }), + ); + onSuccess?.(); break; case MediaRequestStatus.DECLINED: - toast.error(t("jellyseerr.toasts.you_dont_have_permission_to_request")); + toast.error( + t("jellyseerr.toasts.you_dont_have_permission_to_request"), + ); break; case MediaRequestStatus.FAILED: - toast.error(t("jellyseerr.toasts.something_went_wrong_requesting_media")); + toast.error( + t("jellyseerr.toasts.something_went_wrong_requesting_media"), + ); break; } }); }, - [jellyseerrApi] + [jellyseerrApi], ); const isJellyseerrResult = ( - items: any | null | undefined + items: any | null | undefined, ): items is Results => { return ( items && - Object.hasOwn(items, "mediaType") && - Object.values(MediaType).includes(items["mediaType"]) - ) + Object.hasOwn(items, "mediaType") && + Object.values(MediaType).includes(items["mediaType"]) + ); }; - const getTitle = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { + const getTitle = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ) => { return isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name) - : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) + ? item.mediaType == MediaType.MOVIE + ? item?.title + : item?.name + : item?.mediaInfo.mediaType == MediaType.MOVIE + ? (item as MovieDetails)?.title + : (item as TvDetails)?.name; }; - const getYear = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { - return new Date(( - isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate) - : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate)) - || "" - )?.getFullYear?.() + const getYear = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ) => { + return new Date( + (isJellyseerrResult(item) + ? item.mediaType == MediaType.MOVIE + ? item?.releaseDate + : item?.firstAirDate + : item?.mediaInfo.mediaType == MediaType.MOVIE + ? (item as MovieDetails)?.releaseDate + : (item as TvDetails)?.firstAirDate) || "", + )?.getFullYear?.(); }; - const getMediaType = (item?: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => { + const getMediaType = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ): MediaType => { return isJellyseerrResult(item) ? item.mediaType - : item?.mediaInfo?.mediaType + : item?.mediaInfo?.mediaType; }; const jellyseerrRegion = useMemo( () => jellyseerrUser?.settings?.region || "US", - [jellyseerrUser] + [jellyseerrUser], ); const jellyseerrLocale = useMemo(() => { diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts index 44731ce8..34d1d545 100644 --- a/hooks/useMarkAsPlayed.ts +++ b/hooks/useMarkAsPlayed.ts @@ -1,10 +1,10 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import { useHaptic } from "./useHaptic"; import { useAtom } from "jotai"; +import { useHaptic } from "./useHaptic"; export const useMarkAsPlayed = (items: BaseItemDto[]) => { const [api] = useAtom(apiAtom); @@ -24,10 +24,10 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { ]; items.forEach((item) => { - if(!item.Id) return; + if (!item.Id) return; queriesToInvalidate.push(["item", item.Id]); }); - + queriesToInvalidate.forEach((queryKey) => { queryClient.invalidateQueries({ queryKey }); }); @@ -37,7 +37,7 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { lightHapticFeedback(); items.forEach((item) => { - // Optimistic update + // Optimistic update queryClient.setQueryData( ["item", item.Id], (oldData: BaseItemDto | undefined) => { @@ -51,17 +51,19 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { }; } return oldData; - } + }, ); - }) + }); try { // Process all items - await Promise.all(items.map(item => - played - ? markAsPlayed({ api, item, userId: user?.Id }) - : markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id }) - )); + await Promise.all( + items.map((item) => + played + ? markAsPlayed({ api, item, userId: user?.Id }) + : markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id }), + ), + ); // Bulk invalidate queryClient.invalidateQueries({ @@ -73,19 +75,21 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { "episodes", "seasons", "home", - ...items.map(item => ["item", item.Id]) - ].flat() + ...items.map((item) => ["item", item.Id]), + ].flat(), }); } catch (error) { // Revert all optimistic updates on any failure - items.forEach(item => { + items.forEach((item) => { queryClient.setQueryData( ["item", item.Id], (oldData: BaseItemDto | undefined) => - oldData ? { - ...oldData, - UserData: { ...oldData.UserData, Played: played } - } : oldData + oldData + ? { + ...oldData, + UserData: { ...oldData.UserData, Played: played }, + } + : oldData, ); }); console.error("Error updating played status:", error); diff --git a/hooks/useOrientation.ts b/hooks/useOrientation.ts index dff4015d..a485ec22 100644 --- a/hooks/useOrientation.ts +++ b/hooks/useOrientation.ts @@ -1,5 +1,5 @@ -import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import { useEffect, useState } from "react"; import { Platform } from "react-native"; @@ -7,7 +7,7 @@ export const useOrientation = () => { const [orientation, setOrientation] = useState( Platform.isTV ? ScreenOrientation.OrientationLock.LANDSCAPE - : ScreenOrientation.OrientationLock.UNKNOWN + : ScreenOrientation.OrientationLock.UNKNOWN, ); if (Platform.isTV) return { orientation, setOrientation }; @@ -16,7 +16,7 @@ export const useOrientation = () => { const orientationSubscription = ScreenOrientation.addOrientationChangeListener((event) => { setOrientation( - orientationToOrientationLock(event.orientationInfo.orientation) + orientationToOrientationLock(event.orientationInfo.orientation), ); }); diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index 22ea02ce..67c8777d 100644 --- a/hooks/useRemuxHlsToMp4.ts +++ b/hooks/useRemuxHlsToMp4.ts @@ -2,7 +2,7 @@ import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getItemImage } from "@/utils/getItemImage"; import { writeErrorLog, writeInfoLog, writeToLog } from "@/utils/log"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -14,16 +14,16 @@ import { useRouter } from "expo-router"; const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; +import { useSettings } from "@/utils/atoms/settings"; +import useDownloadHelper from "@/utils/download"; +import type { JobStatus } from "@/utils/optimize-server"; +import type { Api } from "@jellyfin/sdk"; import { useAtomValue } from "jotai"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { toast } from "sonner-native"; import useImageStorage from "./useImageStorage"; -import useDownloadHelper from "@/utils/download"; -import { Api } from "@jellyfin/sdk"; -import { useSettings } from "@/utils/atoms/settings"; -import { JobStatus } from "@/utils/optimize-server"; -import { Platform } from "react-native"; -import { useTranslation } from "react-i18next"; type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession; type Statistics = typeof FFMPEGKitReactNative.Statistics; @@ -107,7 +107,7 @@ export const useRemuxHlsToMp4 = () => { setProcesses((prev: any[]) => { return prev.filter( (process: { itemId: string | undefined }) => - process.itemId !== item.Id + process.itemId !== item.Id, ); }); } catch (e) { @@ -116,7 +116,7 @@ export const useRemuxHlsToMp4 = () => { console.log("completeCallback ~ end"); }, - [processes, setProcesses] + [processes, setProcesses], ); const statisticsCallback = useCallback( @@ -146,13 +146,13 @@ export const useRemuxHlsToMp4 = () => { }); }); }, - [setProcesses, completeCallback] + [setProcesses, completeCallback], ); const startRemuxing = useCallback( async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => { const cacheDir = await FileSystem.getInfoAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); if (!cacheDir.exists) { await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, { @@ -178,7 +178,7 @@ export const useRemuxHlsToMp4 = () => { toast.dismiss(); }, }, - } + }, ); try { @@ -201,25 +201,25 @@ export const useRemuxHlsToMp4 = () => { createFFmpegCommand(url, output).join(" "), (session: any) => completeCallback(session, item), undefined, - (s: any) => statisticsCallback(s, item) + (s: any) => statisticsCallback(s, item), ); } catch (e) { const error = e as Error; console.error("Failed to remux:", error); writeErrorLog( `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}, - Error: ${error.message}, Stack: ${error.stack}` + Error: ${error.message}, Stack: ${error.stack}`, ); setProcesses((prev: any[]) => { return prev.filter( (process: { itemId: string | undefined }) => - process.itemId !== item.Id + process.itemId !== item.Id, ); }); throw error; // Re-throw the error to propagate it to the caller } }, - [settings, processes, setProcesses, completeCallback, statisticsCallback] + [settings, processes, setProcesses, completeCallback, statisticsCallback], ); const cancelRemuxing = useCallback(() => { diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index c0337164..6552f6ea 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -1,8 +1,8 @@ -import { useQuery } from "@tanstack/react-query"; import { apiAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { userAtom } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; import { Platform } from "react-native"; const Notifications = !Platform.isTV ? require("expo-notifications") : null; @@ -11,7 +11,10 @@ export interface useSessionsProps { activeWithinSeconds: number; } -export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => { +export const useSessions = ({ + refetchInterval = 5 * 1000, + activeWithinSeconds = 360, +}: useSessionsProps) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -24,13 +27,17 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = const response = await getSessionApi(api).getSessions({ activeWithinSeconds: activeWithinSeconds, }); - + const result = response.data .filter((s) => s.NowPlayingItem) - .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); - + .sort((a, b) => + (b.NowPlayingItem?.Name ?? "").localeCompare( + a.NowPlayingItem?.Name ?? "", + ), + ); + // Notifications.setBadgeCountAsync(result.length); - return result + return result; }, refetchInterval: refetchInterval, }); diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts index 9bb3630f..e221b49e 100644 --- a/hooks/useTrickplay.ts +++ b/hooks/useTrickplay.ts @@ -1,6 +1,6 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { ticksToMs } from "@/utils/time"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useCallback, useMemo, useRef, useState } from "react"; @@ -107,7 +107,7 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => { setTrickPlayUrl(newTrickPlayUrl); return newTrickPlayUrl; }, - [trickplayInfo, item, api, enabled] + [trickplayInfo, item, api, enabled], ); const prefetchAllTrickplayImages = useCallback(() => { diff --git a/hooks/useWebsockets.ts b/hooks/useWebsockets.ts index d9e6096a..70c91b4f 100644 --- a/hooks/useWebsockets.ts +++ b/hooks/useWebsockets.ts @@ -1,8 +1,8 @@ -import { useEffect } from "react"; -import { Alert } from "react-native"; -import { useRouter } from "expo-router"; import { useWebSocketContext } from "@/providers/WebSocketProvider"; +import { useRouter } from "expo-router"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { Alert } from "react-native"; interface UseWebSocketProps { isPlaying: boolean; @@ -42,7 +42,7 @@ export const useWebSocket = ({ console.log("Command ~ DisplayMessage"); const title = json?.Data?.Arguments?.Header; const body = json?.Data?.Arguments?.Text; - Alert.alert(t("player.message_from_server", {message: title}), body); + Alert.alert(t("player.message_from_server", { message: title }), body); } }; diff --git a/i18n.ts b/i18n.ts index 806668c6..47246a59 100644 --- a/i18n.ts +++ b/i18n.ts @@ -1,20 +1,20 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import { getLocales } from "expo-localization"; import de from "./translations/de.json"; import en from "./translations/en.json"; import es from "./translations/es.json"; import fr from "./translations/fr.json"; import it from "./translations/it.json"; import ja from "./translations/ja.json"; -import tr from "./translations/tr.json"; import nl from "./translations/nl.json"; import pl from "./translations/pl.json"; import sv from "./translations/sv.json"; -import ua from "./translations/ua.json" -import zhCN from './translations/zh-CN.json'; -import zhTW from './translations/zh-TW.json'; -import { getLocales } from "expo-localization"; +import tr from "./translations/tr.json"; +import ua from "./translations/ua.json"; +import zhCN from "./translations/zh-CN.json"; +import zhTW from "./translations/zh-TW.json"; export const APP_LANGUAGES = [ { label: "Deutsch", value: "de" }, diff --git a/modules/VlcPlayerView.tsx b/modules/VlcPlayerView.tsx index 83cacb19..a08ed44e 100644 --- a/modules/VlcPlayerView.tsx +++ b/modules/VlcPlayerView.tsx @@ -1,13 +1,13 @@ import { requireNativeViewManager } from "expo-modules-core"; import * as React from "react"; -import { +import { VideoPlayer, useSettings } from "@/utils/atoms/settings"; +import { Platform } from "react-native"; +import type { + VlcPlayerSource, VlcPlayerViewProps, VlcPlayerViewRef, - VlcPlayerSource, } from "./VlcPlayer.types"; -import {useSettings, VideoPlayer} from "@/utils/atoms/settings"; -import {Platform} from "react-native"; interface NativeViewRef extends VlcPlayerViewRef { setNativeProps?: (props: Partial) => void; @@ -23,13 +23,13 @@ const NativeView = React.forwardRef( if (Platform.OS === "ios" || Platform.isTVOS) { if (settings.defaultPlayer == VideoPlayer.VLC_3) { - console.log("[Apple] Using Vlc Player 3") - return + console.log("[Apple] Using Vlc Player 3"); + return ; } } - console.log("Using default Vlc Player") - return - } + console.log("Using default Vlc Player"); + return ; + }, ); const VlcPlayerView = React.forwardRef( @@ -38,7 +38,7 @@ const VlcPlayerView = React.forwardRef( React.useImperativeHandle(ref, () => ({ startPictureInPicture: async () => { - await nativeRef.current?.startPictureInPicture() + await nativeRef.current?.startPictureInPicture(); }, play: async () => { await nativeRef.current?.play(); @@ -143,7 +143,7 @@ const VlcPlayerView = React.forwardRef( onPipStarted={onPipStarted} /> ); - } + }, ); export default VlcPlayerView; diff --git a/modules/index.ts b/modules/index.ts index 397fc0eb..63eb373e 100644 --- a/modules/index.ts +++ b/modules/index.ts @@ -1,16 +1,16 @@ -import VlcPlayerView from "./VlcPlayerView"; import { + ChapterInfo, PlaybackStatePayload, ProgressUpdatePayload, - VideoLoadStartPayload, - VideoStateChangePayload, - VideoProgressPayload, - VlcPlayerSource, TrackInfo, - ChapterInfo, + VideoLoadStartPayload, + VideoProgressPayload, + VideoStateChangePayload, + VlcPlayerSource, VlcPlayerViewProps, VlcPlayerViewRef, } from "./VlcPlayer.types"; +import VlcPlayerView from "./VlcPlayerView"; export { VlcPlayerView, diff --git a/modules/vlc-player-3/src/VlcPlayer3Module.ts b/modules/vlc-player-3/src/VlcPlayer3Module.ts index b292aaff..c0501304 100644 --- a/modules/vlc-player-3/src/VlcPlayer3Module.ts +++ b/modules/vlc-player-3/src/VlcPlayer3Module.ts @@ -1,5 +1,5 @@ -import { requireNativeModule } from 'expo-modules-core'; +import { requireNativeModule } from "expo-modules-core"; // It loads the native module object from the JSI or falls back to // the bridge module (from NativeModulesProxy) if the remote debugger is on. -export default requireNativeModule('VlcPlayer3'); +export default requireNativeModule("VlcPlayer3"); diff --git a/modules/vlc-player/src/VlcPlayerModule.ts b/modules/vlc-player/src/VlcPlayerModule.ts index 1db9b184..be5b1b65 100644 --- a/modules/vlc-player/src/VlcPlayerModule.ts +++ b/modules/vlc-player/src/VlcPlayerModule.ts @@ -1,5 +1,5 @@ -import { requireNativeModule } from 'expo-modules-core'; +import { requireNativeModule } from "expo-modules-core"; // It loads the native module object from the JSI or falls back to // the bridge module (from NativeModulesProxy) if the remote debugger is on. -export default requireNativeModule('VlcPlayer'); +export default requireNativeModule("VlcPlayer"); diff --git a/package.json b/package.json index 1d4c9fe8..5df34655 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "android:tv": "EXPO_TV=1 expo run:android", "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", - "test": "jest --watchAll", + "prepare": "husky", "postinstall": "patch-package", "lint": "biome format --write ." }, @@ -111,6 +111,7 @@ }, "devDependencies": { "@babel/core": "^7.26.8", + "@biomejs/biome": "^1.9.4", "@react-native-community/cli": "15.1.3", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.14", @@ -119,18 +120,20 @@ "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.0.0", "@types/uuid": "^10.0.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.0", "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", - "typescript": "~5.7.3", - "@biomejs/biome": "^1.9.4" + "typescript": "~5.7.3" }, "private": true, "expo": { "install": { - "exclude": [ - "react-native" - ] + "exclude": ["react-native"] } + }, + "lint-staged": { + "*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"] } } diff --git a/plugins/withAndroidManifest.js b/plugins/withAndroidManifest.js index acc1192a..1896f813 100644 --- a/plugins/withAndroidManifest.js +++ b/plugins/withAndroidManifest.js @@ -1,7 +1,9 @@ -const { withAndroidManifest: NativeAndroidManifest } = require("@expo/config-plugins"); +const { + withAndroidManifest: NativeAndroidManifest, +} = require("@expo/config-plugins"); const withAndroidManifest = (config) => - NativeAndroidManifest(config, async (config) => { + NativeAndroidManifest(config, async (config) => { const mainApplication = config.modResults.manifest.application[0]; // Initialize activity array if it doesn't exist @@ -9,27 +11,30 @@ const withAndroidManifest = (config) => mainApplication.activity = []; } - const googleCastActivityExists = mainApplication.activity.some(activity => - activity.$?.["android:name"] === "com.reactnative.googlecast.RNGCExpandedControllerActivity" + const googleCastActivityExists = mainApplication.activity.some( + (activity) => + activity.$?.["android:name"] === + "com.reactnative.googlecast.RNGCExpandedControllerActivity", ); // Only add the activity if it doesn't already exist if (!googleCastActivityExists) { mainApplication.activity.push({ $: { - "android:name": "com.reactnative.googlecast.RNGCExpandedControllerActivity", + "android:name": + "com.reactnative.googlecast.RNGCExpandedControllerActivity", "android:theme": "@style/Theme.MaterialComponents.NoActionBar", "android:launchMode": "singleTask", }, }); } - const mainActivity = mainApplication.activity.find(activity => - activity.$?.["android:name"] === ".MainActivity" + const mainActivity = mainApplication.activity.find( + (activity) => activity.$?.["android:name"] === ".MainActivity", ); if (mainActivity) { - mainActivity.$["android:supportsPictureInPicture"] = "true" + mainActivity.$["android:supportsPictureInPicture"] = "true"; } return config; diff --git a/plugins/withChangeNativeAndroidTextToWhite.js b/plugins/withChangeNativeAndroidTextToWhite.js index 95c2165a..c84de48a 100644 --- a/plugins/withChangeNativeAndroidTextToWhite.js +++ b/plugins/withChangeNativeAndroidTextToWhite.js @@ -14,12 +14,15 @@ const withChangeNativeAndroidTextToWhite = (expoConfig) => "main", "res", "values", - "styles.xml" + "styles.xml", ); let stylesXml = readFileSync(stylesXmlPath, "utf8"); - stylesXml = stylesXml.replace(/@android:color\/black/g, "@android:color/white"); + stylesXml = stylesXml.replace( + /@android:color\/black/g, + "@android:color/white", + ); writeFileSync(stylesXmlPath, stylesXml, { encoding: "utf8" }); } @@ -27,4 +30,4 @@ const withChangeNativeAndroidTextToWhite = (expoConfig) => }, ]); -module.exports = withChangeNativeAndroidTextToWhite; \ No newline at end of file +module.exports = withChangeNativeAndroidTextToWhite; diff --git a/plugins/withGradleProperties.js b/plugins/withGradleProperties.js index 38ca08ac..23e4e34f 100644 --- a/plugins/withGradleProperties.js +++ b/plugins/withGradleProperties.js @@ -1,19 +1,20 @@ -const { withGradleProperties } = require('expo/config-plugins'); +const { withGradleProperties } = require("expo/config-plugins"); function setGradlePropertiesValue(config, key, value) { - return withGradleProperties(config, exportedConfig => { + return withGradleProperties(config, (exportedConfig) => { const props = exportedConfig.modResults; - const keyIdx = props.findIndex(item => item.type === 'property' && item.key === key); + const keyIdx = props.findIndex( + (item) => item.type === "property" && item.key === key, + ); const property = { - type: 'property', + type: "property", key, - value + value, }; if (keyIdx >= 0) { props.splice(keyIdx, 1, property); - } - else { + } else { props.push(property); } @@ -24,17 +25,13 @@ function setGradlePropertiesValue(config, key, value) { module.exports = function withCustomPlugin(config) { // Expo 52 is not setting this // https://github.com/expo/expo/issues/32558 - config = setGradlePropertiesValue( - config, - 'android.enableJetifier', - 'true', - ); + config = setGradlePropertiesValue(config, "android.enableJetifier", "true"); // Increase memory config = setGradlePropertiesValue( - config, - 'org.gradle.jvmargs', - '-Xmx4096m -XX:MaxMetaspaceSize=1024m', + config, + "org.gradle.jvmargs", + "-Xmx4096m -XX:MaxMetaspaceSize=1024m", ); return config; -}; \ No newline at end of file +}; diff --git a/plugins/withRNBackgroundDownloader.js b/plugins/withRNBackgroundDownloader.js index 1970ceb7..9185321b 100644 --- a/plugins/withRNBackgroundDownloader.js +++ b/plugins/withRNBackgroundDownloader.js @@ -16,12 +16,12 @@ function withRNBackgroundDownloader(expoConfig) { // Find the index of the AppDelegate import statement const importIndex = appDelegateLines.findIndex((line) => - /^#import "AppDelegate.h"/.test(line) + /^#import "AppDelegate.h"/.test(line), ); // Find the index of the last line before the @end statement const endStatementIndex = appDelegateLines.findIndex((line) => - /@end/.test(line) + /@end/.test(line), ); // Insert the import statement if it's not already present @@ -34,7 +34,7 @@ function withRNBackgroundDownloader(expoConfig) { appDelegateLines.splice( endStatementIndex, 0, - backgroundDownloaderDelegate + backgroundDownloaderDelegate, ); } diff --git a/plugins/withTrustLocalCerts.js b/plugins/withTrustLocalCerts.js index 13b326af..5cc22d4c 100644 --- a/plugins/withTrustLocalCerts.js +++ b/plugins/withTrustLocalCerts.js @@ -18,7 +18,7 @@ async function setCustomConfigAsync(config, androidManifest) { const res_file_path = path.join( await Paths.getResourceFolderAsync(config.modRequest.projectRoot), "xml", - "network_security_config.xml" + "network_security_config.xml", ); const res_dir = path.resolve(res_file_path, ".."); @@ -31,7 +31,7 @@ async function setCustomConfigAsync(config, androidManifest) { await fsPromises.copyFile(src_file_path, res_file_path); } catch (e) { throw new Error( - `Failed to copy network security config file from ${src_file_path} to ${res_file_path}: ${e.message}` + `Failed to copy network security config file from ${src_file_path} to ${res_file_path}: ${e.message}`, ); } const mainApplication = getMainApplicationOrThrow(androidManifest); diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index d59ebdeb..8c615101 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -7,14 +7,14 @@ import { getItemImage } from "@/utils/getItemImage"; import { useLog, writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { + type JobStatus, cancelAllJobs, cancelJobById, deleteDownloadItemInfoFromDiskTmp, getAllJobsByDeviceId, getDownloadItemInfoFromDiskTmp, - JobStatus, } from "@/utils/optimize-server"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -23,11 +23,12 @@ import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import * as Application from "expo-application"; import * as FileSystem from "expo-file-system"; -import { FileInfo } from "expo-file-system"; +import type { FileInfo } from "expo-file-system"; import Notifications from "expo-notifications"; import { useRouter } from "expo-router"; import { atom, useAtom } from "jotai"; -import React, { +import type React from "react"; +import { createContext, useCallback, useContext, @@ -35,7 +36,7 @@ import React, { useMemo, } from "react"; import { useTranslation } from "react-i18next"; -import { AppState, AppStateStatus, Platform } from "react-native"; +import { AppState, type AppStateStatus, Platform } from "react-native"; import { toast } from "sonner-native"; import { apiAtom } from "./JellyfinProvider"; @@ -113,12 +114,12 @@ function useDownloadProvider() { .filter((p) => jobs.some((j) => j.id === p.id)); const updatedProcesses = jobs.filter( - (j) => !downloadingProcesses.some((p) => p.id === j.id) + (j) => !downloadingProcesses.some((p) => p.id === j.id), ); setProcesses([...updatedProcesses, ...downloadingProcesses]); - for (let job of jobs) { + for (const job of jobs) { const process = processes.find((p) => p.id === job.id); if ( process && @@ -140,7 +141,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); Notifications.scheduleNotificationAsync({ content: { @@ -188,7 +189,7 @@ function useDownloadProvider() { console.error(error); } }, - [settings?.optimizedVersionsServerUrl, authHeader] + [settings?.optimizedVersionsServerUrl, authHeader], ); const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`; @@ -206,8 +207,8 @@ function useDownloadProvider() { status: "downloading", progress: 0, } - : p - ) + : p, + ), ); BackGroundDownloader?.setConfig({ @@ -230,7 +231,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); const baseDirectory = FileSystem.documentDirectory; @@ -250,8 +251,8 @@ function useDownloadProvider() { status: "downloading", progress: 0, } - : p - ) + : p, + ), ); }) .progress((data) => { @@ -265,14 +266,14 @@ function useDownloadProvider() { status: "downloading", progress: percent, } - : p - ) + : p, + ), ); }) .done(async (doneHandler) => { await saveDownloadedItemInfo( process.item, - doneHandler.bytesDownloaded + doneHandler.bytesDownloaded, ); toast.success( t("home.downloads.toasts.download_completed_for_item", { @@ -287,7 +288,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); setTimeout(() => { BackGroundDownloader.completeHandler(process.id); @@ -308,7 +309,7 @@ function useDownloadProvider() { t("home.downloads.toasts.download_failed_for_item", { item: process.item.Name, error: errorMsg, - }) + }), ); writeToLog("ERROR", `Download failed for ${process.item.Name}`, { error, @@ -323,7 +324,7 @@ function useDownloadProvider() { }); }); }, - [queryClient, settings?.optimizedVersionsServerUrl, authHeader] + [queryClient, settings?.optimizedVersionsServerUrl, authHeader], ); const startBackgroundDownload = useCallback( @@ -359,7 +360,7 @@ function useDownloadProvider() { "Content-Type": "application/json", Authorization: authHeader, }, - } + }, ); if (response.status !== 201) { @@ -378,7 +379,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); } catch (error) { writeToLog("ERROR", "Error in startBackgroundDownload", error); @@ -394,13 +395,13 @@ function useDownloadProvider() { t("home.downloads.toasts.failed_to_start_download_for_item", { item: item.Name, message: error.message, - }) + }), ); if (error.response) { toast.error( t("home.downloads.toasts.server_responded_with_status", { statusCode: error.response.status, - }) + }), ); } else if (error.request) { t("home.downloads.toasts.no_response_received_from_server"); @@ -412,13 +413,13 @@ function useDownloadProvider() { toast.error( t( "home.downloads.toasts.failed_to_start_download_for_item_unexpected_error", - { item: item.Name } - ) + { item: item.Name }, + ), ); } } }, - [settings?.optimizedVersionsServerUrl, authHeader] + [settings?.optimizedVersionsServerUrl, authHeader], ); const deleteAllFiles = async (): Promise => { @@ -431,24 +432,24 @@ function useDownloadProvider() { .then(() => toast.success( t( - "home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully" - ) - ) + "home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully", + ), + ), ) .catch((reason) => { console.error("Failed to delete all files, folders, and jobs:", reason); toast.error( t( - "home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs" - ) + "home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs", + ), ); }); }; const forEveryDocumentDirFile = async ( - includeMMKV: boolean = true, + includeMMKV = true, ignoreList: string[] = [], - callback: (file: FileInfo) => void + callback: (file: FileInfo) => void, ) => { const baseDirectory = FileSystem.documentDirectory; if (!baseDirectory) { @@ -553,7 +554,7 @@ function useDownloadProvider() { } catch (error) { console.error( `Failed to delete file and storage entry for ID ${id}:`, - error + error, ); } }; @@ -563,17 +564,17 @@ function useDownloadProvider() { items.map((i) => { if (i.Id) return deleteFile(i.Id); return; - }) + }), ).then(() => successHapticFeedback()); }; const cleanCacheDirectory = async () => { const cacheDir = await FileSystem.getInfoAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); if (cacheDir.exists) { const cachedFiles = await FileSystem.readDirectoryAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); let position = 0; const batchSize = 3; @@ -584,14 +585,14 @@ function useDownloadProvider() { await Promise.all( itemsForBatch.map(async (file) => { const info = await FileSystem.getInfoAsync( - `${APP_CACHE_DOWNLOAD_DIRECTORY}${file}` + `${APP_CACHE_DOWNLOAD_DIRECTORY}${file}`, ); if (info.exists) { await FileSystem.deleteAsync(info.uri, { idempotent: true }); return Promise.resolve(file); } return Promise.reject(); - }) + }), ); position += batchSize; @@ -609,24 +610,24 @@ function useDownloadProvider() { promises.push(deleteFile(file.item.SeriesId)); promises.push(deleteFile(file.item.Id!)); return promises; - }) || [] + }) || [], ); }; const appSizeUsage = useMemo(async () => { const sizes: number[] = downloadedFiles?.map((d) => { - return getDownloadedItemSize(d.item.Id!!); + return getDownloadedItemSize(d.item.Id!); }) || []; await forEveryDocumentDirFile( true, - getAllDownloadedItems().map((d) => d.item.Id!!), + getAllDownloadedItems().map((d) => d.item.Id!), (file) => { if (file.exists) { sizes.push(file.size); } - } + }, ).catch((e) => { console.error(e); }); @@ -663,10 +664,10 @@ function useDownloadProvider() { } } - function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) { + function saveDownloadedItemInfo(item: BaseItemDto, size = 0) { try { const downloadedItems = storage.getString("downloadedItems"); - let items: DownloadedItem[] = downloadedItems + const items: DownloadedItem[] = downloadedItems ? JSON.parse(downloadedItems) : []; @@ -676,7 +677,7 @@ function useDownloadProvider() { if (!data?.mediaSource) throw new Error( - "Media source not found in tmp storage. Did you forget to save it before starting download?" + "Media source not found in tmp storage. Did you forget to save it before starting download?", ); const newItem = { item, mediaSource: data.mediaSource }; @@ -697,14 +698,14 @@ function useDownloadProvider() { } catch (error) { console.error( "Failed to save downloaded item information with media source:", - error + error, ); } } function getDownloadedItemSize(itemId: string): number { const size = storage.getString("downloadedItemSize-" + itemId); - return size ? parseInt(size) : 0; + return size ? Number.parseInt(size) : 0; } return { diff --git a/providers/DownloadProvider.tv.tsx b/providers/DownloadProvider.tv.tsx index 80d72026..c6319ea3 100644 --- a/providers/DownloadProvider.tv.tsx +++ b/providers/DownloadProvider.tv.tsx @@ -1,13 +1,14 @@ import { storage } from "@/utils/mmkv"; -import { JobStatus } from "@/utils/optimize-server"; -import { +import type { JobStatus } from "@/utils/optimize-server"; +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import * as Application from "expo-application"; import * as FileSystem from "expo-file-system"; import { atom, useAtom } from "jotai"; -import React, { createContext, useCallback, useContext, useMemo } from "react"; +import type React from "react"; +import { createContext, useCallback, useContext, useMemo } from "react"; export type DownloadedItem = { item: Partial; @@ -38,7 +39,7 @@ function useDownloadProvider() { async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => { return null; }, - [] + [], ); const deleteAllFiles = async (): Promise => {}; @@ -59,11 +60,11 @@ function useDownloadProvider() { return null; } - function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) {} + function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {} function getDownloadedItemSize(itemId: string): number { const size = storage.getString("downloadedItemSize-" + itemId); - return size ? parseInt(size) : 0; + return size ? Number.parseInt(size) : 0; } const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 1256d2ae..b8bab659 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -2,19 +2,21 @@ import "@/augmentations"; import { useInterval } from "@/hooks/useInterval"; import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { useSettings } from "@/utils/atoms/settings"; +import { writeErrorLog, writeInfoLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { store } from "@/utils/store"; -import { Api, Jellyfin } from "@jellyfin/sdk"; -import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { type Api, Jellyfin } from "@jellyfin/sdk"; +import type { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserApi } from "@jellyfin/sdk/lib/utils/api"; import { useMutation, useQuery } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; import { router, useSegments } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import { atom, useAtom } from "jotai"; -import React, { +import type React from "react"; +import { + type ReactNode, createContext, - ReactNode, useCallback, useContext, useEffect, @@ -25,7 +27,6 @@ import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { getDeviceName } from "react-native-device-info"; import uuid from "react-native-uuid"; -import {writeErrorLog, writeInfoLog} from "@/utils/log"; interface Server { address: string; @@ -45,7 +46,7 @@ interface JellyfinContextValue { } const JellyfinContext = createContext( - undefined + undefined, ); export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ @@ -68,7 +69,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ name: deviceName, id, }, - }) + }), ); setDeviceId(id); })(); @@ -104,7 +105,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ null, { headers, - } + }, ); if (response?.status === 200) { setSecret(response?.data?.Secret); @@ -124,7 +125,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ try { const response = await api.axiosInstance.get( - `${api.basePath}/QuickConnect/Connect?Secret=${secret}` + `${api.basePath}/QuickConnect/Connect?Secret=${secret}`, ); if (response.status === 200) { @@ -138,7 +139,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ }, { headers, - } + }, ); const { AccessToken, User } = authResponse.data; @@ -167,7 +168,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ await refreshStreamyfinPluginSettings(); })(); }, []); - + useEffect(() => { store.set(apiAtom, api); }, [api]); @@ -176,9 +177,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min const discoverServers = async (url: string): Promise => { - const servers = await jellyfin?.discovery.getRecommendedServerCandidates( - url - ); + const servers = + await jellyfin?.discovery.getRecommendedServerCandidates(url); return servers?.map((server) => ({ address: server.address })) || []; }; @@ -193,7 +193,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ }, onSuccess: (_, server) => { const previousServers = JSON.parse( - storage.getString("previousServers") || "[]" + storage.getString("previousServers") || "[]", ); const updatedServers = [ server, @@ -201,7 +201,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ ]; storage.set( "previousServers", - JSON.stringify(updatedServers.slice(0, 5)) + JSON.stringify(updatedServers.slice(0, 5)), ); }, onError: (error) => { @@ -241,7 +241,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const recentPluginSettings = await refreshStreamyfinPluginSettings(); if (recentPluginSettings?.jellyseerrServerUrl?.value) { const jellyseerrApi = new JellyseerrApi( - recentPluginSettings.jellyseerrServerUrl.value + recentPluginSettings.jellyseerrServerUrl.value, ); await jellyseerrApi.test().then((result) => { if (result.isValid && result.requiresPass) { @@ -257,23 +257,23 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ throw new Error(t("login.invalid_username_or_password")); case 403: throw new Error( - t("login.user_does_not_have_permission_to_log_in") + t("login.user_does_not_have_permission_to_log_in"), ); case 408: throw new Error( - t("login.server_is_taking_too_long_to_respond_try_again_later") + t("login.server_is_taking_too_long_to_respond_try_again_later"), ); case 429: throw new Error( - t("login.server_received_too_many_requests_try_again_later") + t("login.server_received_too_many_requests_try_again_later"), ); case 500: throw new Error(t("login.there_is_a_server_error")); default: throw new Error( t( - "login.an_unexpected_error_occured_did_you_enter_the_correct_url" - ) + "login.an_unexpected_error_occured_did_you_enter_the_correct_url", + ), ); } } @@ -287,9 +287,12 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const logoutMutation = useMutation({ mutationFn: async () => { - api?.delete(`/Streamyfin/device/${deviceId}`) - .then(r => writeInfoLog("Deleted expo push token for device")) - .catch(e => writeErrorLog(`Failed to delete expo push token for device`)) + api + ?.delete(`/Streamyfin/device/${deviceId}`) + .then((r) => writeInfoLog("Deleted expo push token for device")) + .catch((e) => + writeErrorLog(`Failed to delete expo push token for device`), + ); storage.delete("token"); setUser(null); diff --git a/providers/JobQueueProvider.tsx b/providers/JobQueueProvider.tsx index 00358e48..232a5f02 100644 --- a/providers/JobQueueProvider.tsx +++ b/providers/JobQueueProvider.tsx @@ -1,5 +1,6 @@ -import React, { createContext } from "react"; import { useJobProcessor } from "@/utils/atoms/queue"; +import type React from "react"; +import { createContext } from "react"; const JobQueueContext = createContext(null); diff --git a/providers/PlaySettingsProvider.tsx b/providers/PlaySettingsProvider.tsx index ff80bb9e..fad502fb 100644 --- a/providers/PlaySettingsProvider.tsx +++ b/providers/PlaySettingsProvider.tsx @@ -1,14 +1,15 @@ -import { Bitrate } from "@/components/BitrateSelector"; +import type { Bitrate } from "@/components/BitrateSelector"; import { settingsAtom } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import native from "@/utils/profiles/native"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtomValue } from "jotai"; -import React, { +import type React from "react"; +import { createContext, useCallback, useContext, @@ -32,7 +33,7 @@ type PlaySettingsContextType = { dataOrUpdater: | PlaybackType | null - | ((prev: PlaybackType | null) => PlaybackType | null) + | ((prev: PlaybackType | null) => PlaybackType | null), ) => Promise<{ url: string | null; sessionId: string | null } | null>; playUrl?: string | null; setPlayUrl: React.Dispatch>; @@ -41,7 +42,7 @@ type PlaySettingsContextType = { }; const PlaySettingsContext = createContext( - undefined + undefined, ); export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ @@ -65,7 +66,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ dataOrUpdater: | PlaybackType | null - | ((prev: PlaybackType | null) => PlaybackType | null) + | ((prev: PlaybackType | null) => PlaybackType | null), ): Promise<{ url: string | null; sessionId: string | null } | null> => { if (!api || !user || !settings) { _setPlaySettings(null); @@ -109,7 +110,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ return null; } }, - [api, user, settings, playSettings] + [api, user, settings, playSettings], ); // useEffect(() => { @@ -153,7 +154,7 @@ export const usePlaySettings = () => { const context = useContext(PlaySettingsContext); if (context === undefined) { throw new Error( - "usePlaySettings must be used within a PlaySettingsProvider" + "usePlaySettings must be used within a PlaySettingsProvider", ); } return context; diff --git a/providers/WebSocketProvider.tsx b/providers/WebSocketProvider.tsx index b61c1967..c5857e9a 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -1,19 +1,16 @@ +import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; +import { useAtomValue } from "jotai"; import React, { createContext, useContext, useEffect, useState, - ReactNode, + type ReactNode, useMemo, useCallback, } from "react"; -import { AppState, AppStateStatus } from "react-native"; -import { useAtomValue } from "jotai"; -import { - apiAtom, - getOrSetDeviceId, -} from "@/providers/JellyfinProvider"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; +import { AppState, type AppStateStatus } from "react-native"; interface WebSocketProviderProps { children: ReactNode; @@ -121,7 +118,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const subscription = AppState.addEventListener( "change", - handleAppStateChange + handleAppStateChange, ); return () => { @@ -141,7 +138,7 @@ export const useWebSocketContext = (): WebSocketContextType => { const context = useContext(WebSocketContext); if (!context) { throw new Error( - "useWebSocketContext must be used within a WebSocketProvider" + "useWebSocketContext must be used within a WebSocketProvider", ); } return context; diff --git a/scripts/symlink-native-dirs.js b/scripts/symlink-native-dirs.js index d7819611..9b5c8133 100644 --- a/scripts/symlink-native-dirs.js +++ b/scripts/symlink-native-dirs.js @@ -25,22 +25,22 @@ const paths = new Map([ if (isTV) { stdout = execSync( - `mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios` + `mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios`, ); console.log(stdout.toString()); stdout = execSync( `mkdir -p ${paths.get("androidtv")}; ln -nsf ${paths.get( - "androidtv" - )} android` + "androidtv", + )} android`, ); console.log(stdout.toString()); } else { stdout = execSync( - `mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios` + `mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios`, ); console.log(stdout.toString()); stdout = execSync( - `mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android` + `mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android`, ); console.log(stdout.toString()); } diff --git a/translations/de.json b/translations/de.json index d48e134e..993c2176 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,473 +1,473 @@ -{ - "login": { - "username_required": "Benutzername ist erforderlich", - "error_title": "Fehler", - "login_title": "Anmelden", - "login_to_title": "Anmelden bei", - "username_placeholder": "Benutzername", - "password_placeholder": "Passwort", - "login_button": "Anmelden", - "quick_connect": "Schnellverbindung", - "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", - "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", - "got_it": "Verstanden", - "connection_failed": "Verbindung fehlgeschlagen", - "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", - "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", - "change_server": "Server wechseln", - "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", - "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", - "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", - "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", - "there_is_a_server_error": "Es gibt einen Serverfehler", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" - }, - "server": { - "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", - "server_url_placeholder": "http(s)://dein-server.de", - "connect_button": "Verbinden", - "previous_servers": "Vorherige Server", - "clear_button": "Löschen", - "search_for_local_servers": "Nach lokalen Servern suchen", - "searching": "Suche...", - "servers": "Server" - }, - "home": { - "no_internet": "Kein Internet", - "no_items": "Keine Elemente", - "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", - "go_to_downloads": "Gehe zu den Downloads", - "oops": "Ups!", - "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", - "continue_watching": "Weiterschauen", - "next_up": "Als nächstes", - "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", - "suggested_movies": "Empfohlene Filme", - "suggested_episodes": "Empfohlene Episoden", - "intro": { - "welcome_to_streamyfin": "Willkommen bei Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", - "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", - "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", - "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", - "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", - "done_button": "Fertig", - "go_to_settings_button": "Gehe zu den Einstellungen", - "read_more": "Mehr Erfahren" - }, - "settings": { - "settings_title": "Einstellungen", - "log_out_button": "Abmelden", - "user_info": { - "user_info_title": "Benutzerinformationen", - "user": "Benutzer", - "server": "Server", - "token": "Token", - "app_version": "App-Version" - }, - "quick_connect": { - "quick_connect_title": "Schnellverbindung", - "authorize_button": "Schnellverbindung autorisieren", - "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", - "success": "Erfolg", - "quick_connect_autorized": "Schnellverbindung autorisiert", - "error": "Fehler", - "invalid_code": "Ungültiger Code", - "authorize": "Autorisieren" - }, - "media_controls": { - "media_controls_title": "Mediensteuerung", - "forward_skip_length": "Vorspulzeit", - "rewind_length": "Rückspulzeit", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", - "audio_language": "Audio-Sprache", - "audio_hint": "Wähl die Standardsprache für Audio aus.", - "none": "Keine", - "language": "Sprache" - }, - "subtitles": { - "subtitle_title": "Untertitel", - "subtitle_language": "Untertitel-Sprache", - "subtitle_mode": "Untertitel-Modus", - "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", - "subtitle_size": "Untertitel-Größe", - "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", - "none": "Keine", - "language": "Sprache", - "loading": "Lädt", - "modes": { - "Default": "Standard", - "Smart": "Smart", - "Always": "Immer", - "None": "Keine", - "OnlyForced": "Nur erzwungen" - } - }, - "other": { - "other_title": "Sonstiges", - "follow_device_orientation": "Automatische Drehung", - "video_orientation": "Videoausrichtung", - "orientation": "Ausrichtung", - "orientations": { - "DEFAULT": "Standard", - "ALL": "Alle", - "PORTRAIT": "Hochformat", - "PORTRAIT_UP": "Hochformat oben", - "PORTRAIT_DOWN": "Hochformat unten", - "LANDSCAPE": "Querformat", - "LANDSCAPE_LEFT": "Querformat links", - "LANDSCAPE_RIGHT": "Querformat rechts", - "OTHER": "Andere", - "UNKNOWN": "Unbekannt" - }, - "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", - "hide_libraries": "Bibliotheken ausblenden", - "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", - "disable_haptic_feedback": "Haptisches Feedback deaktivieren", - "default_quality": "Standardqualität" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download-Methode", - "remux_max_download": "Maximaler Remux-Download", - "auto_download": "Automatischer Download", - "optimized_versions_server": "Optimierter Versions-Server", - "save_button": "Speichern", - "optimized_server": "Optimierter Server", - "optimized": "Optimiert", - "default": "Standard", - "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", - "server_url": "Server URL", - "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Passwort", - "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", - "save_button": "Speichern", - "clear_button": "Löschen", - "login_button": "Anmelden", - "total_media_requests": "Gesamtanfragen", - "movie_quota_limit": "Film-Anfragelimit", - "movie_quota_days": "Film-Anfragetage", - "tv_quota_limit": "TV-Anfragelimit", - "tv_quota_days": "TV-Anfragetage", - "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", - "unlimited": "Unlimitiert", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Aktiviere Marlin Search", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_marlin": "Erfahre mehr über Marlin.", - "save_button": "Speichern", - "toasts": { - "saved": "Gespeichert" - } - } - }, - "storage": { - "storage_title": "Speicher", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Gerät {{availableSpace}}%", - "size_used": "{{used}} von {{total}} benutzt", - "delete_all_downloaded_files": "Alle Downloads löschen" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Keine Logs verfügbar", - "delete_all_logs": "Alle Logs löschen" - }, - "languages": { - "title": "Sprachen", - "app_language": "App-Sprache", - "app_language_description": "Wähle die Sprache für die App aus.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Fehler beim Löschen von Dateien", - "background_downloads_enabled": "Hintergrunddownloads aktiviert", - "background_downloads_disabled": "Hintergrunddownloads deaktiviert", - "connected": "Verbunden", - "could_not_connect": "Konnte keine Verbindung herstellen", - "invalid_url": "Ungültige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Serien", - "movies": "Filme", - "queue": "Warteschlange", - "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", - "no_items_in_queue": "Keine Elemente in der Warteschlange", - "no_downloaded_items": "Keine heruntergeladenen Elemente", - "delete_all_movies_button": "Alle Filme löschen", - "delete_all_tvseries_button": "Alle TV-Serien löschen", - "delete_all_button": "Alles löschen", - "active_download": "Aktiver Download", - "no_active_downloads": "Keine aktiven Downloads", - "active_downloads": "Aktive Downloads", - "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", - "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", - "back": "Zurück", - "delete": "Löschen", - "something_went_wrong": "Etwas ist schiefgelaufen", - "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", - "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", - "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", - "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", - "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", - "download_cancelled": "Download abgebrochen", - "could_not_cancel_download": "Download konnte nicht abgebrochen werden", - "download_completed": "Download abgeschlossen", - "download_started_for": "Download für {{item}} gestartet", - "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", - "download_stated_for_item": "Download für {{item}} gestartet", - "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", - "download_completed_for_item": "Download für {{item}} ", - "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", - "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", - "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", - "no_response_received_from_server": "Keine Antwort vom Server erhalten", - "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", - "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", - "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", - "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", - "go_to_downloads": "Gehe zu den Downloads" - } - } - }, - "search": { - "search_here": "Hier Suchen...", - "search": "Suche...", - "x_items": "{{count}} Elemente", - "library": "Bibliothek", - "discover": "Entdecken", - "no_results": "Keine Ergebnisse", - "no_results_found_for": "Keine Ergebnisse gefunden für", - "movies": "Filme", - "series": "Serien", - "episodes": "Episoden", - "collections": "Sammlungen", - "actors": "Schauspieler", - "request_movies": "Film anfragen", - "request_series": "Serie anfragen", - "recently_added": "Kürzlich hinzugefügt", - "recent_requests": "Kürzlich angefragt", - "plex_watchlist": "Plex Watchlist", - "trending": "In den Trends", - "popular_movies": "Beliebte Filme", - "movie_genres": "Film-Genres", - "upcoming_movies": "Kommende Filme", - "studios": "Studios", - "popular_tv": "Beliebte TV-Serien", - "tv_genres": "TV-Serien-Genres", - "upcoming_tv": "Kommende TV-Serien", - "networks": "Netzwerke", - "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", - "tmdb_movie_genre": "TMDB Film-Genre", - "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", - "tmdb_tv_genre": "TMDB TV-Serien-Genre", - "tmdb_search": "TMDB Suche", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netzwerk", - "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", - "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" - }, - "library": { - "no_items_found": "Keine Elemente gefunden", - "no_results": "Keine Ergebnisse", - "no_libraries_found": "Keine Bibliotheken gefunden", - "item_types": { - "movies": "Filme", - "series": "Serien", - "boxsets": "Boxsets", - "items": "Elemente" - }, - "options": { - "display": "Display", - "row": "Reihe", - "list": "Liste", - "image_style": "Bildstil", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Titel anzeigen", - "show_stats": "Statistiken anzeigen" - }, - "filters": { - "genres": "Genres", - "years": "Jahre", - "sort_by": "Sortieren nach", - "sort_order": "Sortierreihenfolge", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Serien", - "movies": "Filme", - "episodes": "Episoden", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists", - "noDataTitle": "Noch keine Favoriten", - "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden." - }, - "custom_links": { - "no_links": "Keine Links" - }, - "player": { - "error": "Fehler", - "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", - "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", - "client_error": "Client-Fehler", - "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", - "message_from_server": "Nachricht vom Server: {{message}}", - "video_has_finished_playing": "Video wurde fertig abgespielt!", - "no_video_source": "Keine Videoquelle...", - "next_episode": "Nächste Episode", - "refresh_tracks": "Spuren aktualisieren", - "subtitle_tracks": "Untertitel-Spuren:", - "audio_tracks": "Audiospuren:", - "playback_state": "Wiedergabestatus:", - "no_data_available": "Keine Daten verfügbar", - "index": "Index:" - }, - "item_card": { - "next_up": "Als Nächstes", - "no_items_to_display": "Keine Elemente zum Anzeigen", - "cast_and_crew": "Besetzung und Crew", - "series": "Serien", - "seasons": "Staffeln", - "season": "Staffel", - "no_episodes_for_this_season": "Keine Episoden für diese Staffel", - "overview": "Überblick", - "more_with": "Mehr mit {{name}}", - "similar_items": "Ähnliche Elemente", - "no_similar_items_found": "Keine ähnlichen Elemente gefunden", - "video": "Video", - "more_details": "Mehr Details", - "quality": "Qualität", - "audio": "Audio", - "subtitles": "Untertitel", - "show_more": "Mehr anzeigen", - "show_less": "Weniger anzeigen", - "appeared_in": "Erschienen in", - "could_not_load_item": "Konnte Element nicht laden", - "none": "Keine", - "download": { - "download_season": "Staffel herunterladen", - "download_series": "Serie herunterladen", - "download_episode": "Episode herunterladen", - "download_movie": "Film herunterladen", - "download_x_item": "{{item_count}} Elemente herunterladen", - "download_button": "Herunterladen", - "using_optimized_server": "Verwende optimierten Server", - "using_default_method": "Verwende Standardmethode" - } - }, - "live_tv": { - "next": "Nächster", - "previous": "Vorheriger", - "live_tv": "Live TV", - "coming_soon": "Demnächst", - "on_now": "Jetzt", - "shows": "Shows", - "movies": "Filme", - "sports": "Sport", - "for_kids": "Für Kinder", - "news": "Nachrichten" - }, - "jellyseerr": { - "confirm": "Bestätigen", - "cancel": "Abbrechen", - "yes": "Ja", - "whats_wrong": "Hast du Probleme?", - "issue_type": "Fehlerart", - "select_an_issue": "Wähle einen Fehlerart aus", - "types": "Arten", - "describe_the_issue": "(optional) Beschreibe das Problem", - "submit_button": "Absenden", - "report_issue_button": "Fehler melden", - "request_button": "Anfragen", - "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", - "failed_to_login": "Fehler beim Anmelden", - "cast": "Besetzung", - "details": "Details", - "status": "Status", - "original_title": "Original Titel", - "series_type": "Serien Typ", - "release_dates": "Veröffentlichungsdaten", - "first_air_date": "Erstausstrahlungsdatum", - "next_air_date": "Nächstes Ausstrahlungsdatum", - "revenue": "Einnahmen", - "budget": "Budget", - "original_language": "Originalsprache", - "production_country": "Produktionsland", - "studios": "Studios", - "network": "Netzwerk", - "currently_streaming_on": "Derzeit im Streaming auf", - "advanced": "Erweitert", - "request_as": "Anfragen als", - "tags": "Tags", - "quality_profile": "Qualitätsprofil", - "root_folder": "Root-Ordner", - "season_all": "Season (all)", - "season_number": "Staffel {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Geboren", - "appearances": "Auftritte", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", - "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", - "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", - "issue_submitted": "Problem eingereicht!", - "requested_item": "{{item}} angefragt!", - "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", - "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" - } - }, - "tabs": { - "home": "Startseite", - "search": "Suche", - "library": "Bibliothek", - "custom_links": "Benutzerdefinierte Links", - "favorites": "Favoriten" - } -} +{ + "login": { + "username_required": "Benutzername ist erforderlich", + "error_title": "Fehler", + "login_title": "Anmelden", + "login_to_title": "Anmelden bei", + "username_placeholder": "Benutzername", + "password_placeholder": "Passwort", + "login_button": "Anmelden", + "quick_connect": "Schnellverbindung", + "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", + "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", + "got_it": "Verstanden", + "connection_failed": "Verbindung fehlgeschlagen", + "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", + "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", + "change_server": "Server wechseln", + "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", + "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", + "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", + "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", + "there_is_a_server_error": "Es gibt einen Serverfehler", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" + }, + "server": { + "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", + "server_url_placeholder": "http(s)://dein-server.de", + "connect_button": "Verbinden", + "previous_servers": "Vorherige Server", + "clear_button": "Löschen", + "search_for_local_servers": "Nach lokalen Servern suchen", + "searching": "Suche...", + "servers": "Server" + }, + "home": { + "no_internet": "Kein Internet", + "no_items": "Keine Elemente", + "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", + "go_to_downloads": "Gehe zu den Downloads", + "oops": "Ups!", + "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", + "continue_watching": "Weiterschauen", + "next_up": "Als nächstes", + "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", + "suggested_movies": "Empfohlene Filme", + "suggested_episodes": "Empfohlene Episoden", + "intro": { + "welcome_to_streamyfin": "Willkommen bei Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", + "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", + "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", + "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", + "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", + "done_button": "Fertig", + "go_to_settings_button": "Gehe zu den Einstellungen", + "read_more": "Mehr Erfahren" + }, + "settings": { + "settings_title": "Einstellungen", + "log_out_button": "Abmelden", + "user_info": { + "user_info_title": "Benutzerinformationen", + "user": "Benutzer", + "server": "Server", + "token": "Token", + "app_version": "App-Version" + }, + "quick_connect": { + "quick_connect_title": "Schnellverbindung", + "authorize_button": "Schnellverbindung autorisieren", + "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", + "success": "Erfolg", + "quick_connect_autorized": "Schnellverbindung autorisiert", + "error": "Fehler", + "invalid_code": "Ungültiger Code", + "authorize": "Autorisieren" + }, + "media_controls": { + "media_controls_title": "Mediensteuerung", + "forward_skip_length": "Vorspulzeit", + "rewind_length": "Rückspulzeit", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", + "audio_language": "Audio-Sprache", + "audio_hint": "Wähl die Standardsprache für Audio aus.", + "none": "Keine", + "language": "Sprache" + }, + "subtitles": { + "subtitle_title": "Untertitel", + "subtitle_language": "Untertitel-Sprache", + "subtitle_mode": "Untertitel-Modus", + "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", + "subtitle_size": "Untertitel-Größe", + "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", + "none": "Keine", + "language": "Sprache", + "loading": "Lädt", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Immer", + "None": "Keine", + "OnlyForced": "Nur erzwungen" + } + }, + "other": { + "other_title": "Sonstiges", + "follow_device_orientation": "Automatische Drehung", + "video_orientation": "Videoausrichtung", + "orientation": "Ausrichtung", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Hochformat", + "PORTRAIT_UP": "Hochformat oben", + "PORTRAIT_DOWN": "Hochformat unten", + "LANDSCAPE": "Querformat", + "LANDSCAPE_LEFT": "Querformat links", + "LANDSCAPE_RIGHT": "Querformat rechts", + "OTHER": "Andere", + "UNKNOWN": "Unbekannt" + }, + "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", + "hide_libraries": "Bibliotheken ausblenden", + "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", + "disable_haptic_feedback": "Haptisches Feedback deaktivieren", + "default_quality": "Standardqualität" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download-Methode", + "remux_max_download": "Maximaler Remux-Download", + "auto_download": "Automatischer Download", + "optimized_versions_server": "Optimierter Versions-Server", + "save_button": "Speichern", + "optimized_server": "Optimierter Server", + "optimized": "Optimiert", + "default": "Standard", + "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", + "server_url": "Server URL", + "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Passwort", + "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", + "save_button": "Speichern", + "clear_button": "Löschen", + "login_button": "Anmelden", + "total_media_requests": "Gesamtanfragen", + "movie_quota_limit": "Film-Anfragelimit", + "movie_quota_days": "Film-Anfragetage", + "tv_quota_limit": "TV-Anfragelimit", + "tv_quota_days": "TV-Anfragetage", + "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", + "unlimited": "Unlimitiert", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktiviere Marlin Search", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_marlin": "Erfahre mehr über Marlin.", + "save_button": "Speichern", + "toasts": { + "saved": "Gespeichert" + } + } + }, + "storage": { + "storage_title": "Speicher", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Gerät {{availableSpace}}%", + "size_used": "{{used}} von {{total}} benutzt", + "delete_all_downloaded_files": "Alle Downloads löschen" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Keine Logs verfügbar", + "delete_all_logs": "Alle Logs löschen" + }, + "languages": { + "title": "Sprachen", + "app_language": "App-Sprache", + "app_language_description": "Wähle die Sprache für die App aus.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Fehler beim Löschen von Dateien", + "background_downloads_enabled": "Hintergrunddownloads aktiviert", + "background_downloads_disabled": "Hintergrunddownloads deaktiviert", + "connected": "Verbunden", + "could_not_connect": "Konnte keine Verbindung herstellen", + "invalid_url": "Ungültige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Serien", + "movies": "Filme", + "queue": "Warteschlange", + "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", + "no_items_in_queue": "Keine Elemente in der Warteschlange", + "no_downloaded_items": "Keine heruntergeladenen Elemente", + "delete_all_movies_button": "Alle Filme löschen", + "delete_all_tvseries_button": "Alle TV-Serien löschen", + "delete_all_button": "Alles löschen", + "active_download": "Aktiver Download", + "no_active_downloads": "Keine aktiven Downloads", + "active_downloads": "Aktive Downloads", + "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", + "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", + "back": "Zurück", + "delete": "Löschen", + "something_went_wrong": "Etwas ist schiefgelaufen", + "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", + "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", + "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", + "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", + "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", + "download_cancelled": "Download abgebrochen", + "could_not_cancel_download": "Download konnte nicht abgebrochen werden", + "download_completed": "Download abgeschlossen", + "download_started_for": "Download für {{item}} gestartet", + "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", + "download_stated_for_item": "Download für {{item}} gestartet", + "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", + "download_completed_for_item": "Download für {{item}} ", + "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", + "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", + "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", + "no_response_received_from_server": "Keine Antwort vom Server erhalten", + "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", + "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", + "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", + "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", + "go_to_downloads": "Gehe zu den Downloads" + } + } + }, + "search": { + "search_here": "Hier Suchen...", + "search": "Suche...", + "x_items": "{{count}} Elemente", + "library": "Bibliothek", + "discover": "Entdecken", + "no_results": "Keine Ergebnisse", + "no_results_found_for": "Keine Ergebnisse gefunden für", + "movies": "Filme", + "series": "Serien", + "episodes": "Episoden", + "collections": "Sammlungen", + "actors": "Schauspieler", + "request_movies": "Film anfragen", + "request_series": "Serie anfragen", + "recently_added": "Kürzlich hinzugefügt", + "recent_requests": "Kürzlich angefragt", + "plex_watchlist": "Plex Watchlist", + "trending": "In den Trends", + "popular_movies": "Beliebte Filme", + "movie_genres": "Film-Genres", + "upcoming_movies": "Kommende Filme", + "studios": "Studios", + "popular_tv": "Beliebte TV-Serien", + "tv_genres": "TV-Serien-Genres", + "upcoming_tv": "Kommende TV-Serien", + "networks": "Netzwerke", + "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", + "tmdb_movie_genre": "TMDB Film-Genre", + "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", + "tmdb_tv_genre": "TMDB TV-Serien-Genre", + "tmdb_search": "TMDB Suche", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netzwerk", + "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", + "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" + }, + "library": { + "no_items_found": "Keine Elemente gefunden", + "no_results": "Keine Ergebnisse", + "no_libraries_found": "Keine Bibliotheken gefunden", + "item_types": { + "movies": "Filme", + "series": "Serien", + "boxsets": "Boxsets", + "items": "Elemente" + }, + "options": { + "display": "Display", + "row": "Reihe", + "list": "Liste", + "image_style": "Bildstil", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Titel anzeigen", + "show_stats": "Statistiken anzeigen" + }, + "filters": { + "genres": "Genres", + "years": "Jahre", + "sort_by": "Sortieren nach", + "sort_order": "Sortierreihenfolge", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Serien", + "movies": "Filme", + "episodes": "Episoden", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "Noch keine Favoriten", + "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden." + }, + "custom_links": { + "no_links": "Keine Links" + }, + "player": { + "error": "Fehler", + "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", + "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", + "client_error": "Client-Fehler", + "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", + "message_from_server": "Nachricht vom Server: {{message}}", + "video_has_finished_playing": "Video wurde fertig abgespielt!", + "no_video_source": "Keine Videoquelle...", + "next_episode": "Nächste Episode", + "refresh_tracks": "Spuren aktualisieren", + "subtitle_tracks": "Untertitel-Spuren:", + "audio_tracks": "Audiospuren:", + "playback_state": "Wiedergabestatus:", + "no_data_available": "Keine Daten verfügbar", + "index": "Index:" + }, + "item_card": { + "next_up": "Als Nächstes", + "no_items_to_display": "Keine Elemente zum Anzeigen", + "cast_and_crew": "Besetzung und Crew", + "series": "Serien", + "seasons": "Staffeln", + "season": "Staffel", + "no_episodes_for_this_season": "Keine Episoden für diese Staffel", + "overview": "Überblick", + "more_with": "Mehr mit {{name}}", + "similar_items": "Ähnliche Elemente", + "no_similar_items_found": "Keine ähnlichen Elemente gefunden", + "video": "Video", + "more_details": "Mehr Details", + "quality": "Qualität", + "audio": "Audio", + "subtitles": "Untertitel", + "show_more": "Mehr anzeigen", + "show_less": "Weniger anzeigen", + "appeared_in": "Erschienen in", + "could_not_load_item": "Konnte Element nicht laden", + "none": "Keine", + "download": { + "download_season": "Staffel herunterladen", + "download_series": "Serie herunterladen", + "download_episode": "Episode herunterladen", + "download_movie": "Film herunterladen", + "download_x_item": "{{item_count}} Elemente herunterladen", + "download_button": "Herunterladen", + "using_optimized_server": "Verwende optimierten Server", + "using_default_method": "Verwende Standardmethode" + } + }, + "live_tv": { + "next": "Nächster", + "previous": "Vorheriger", + "live_tv": "Live TV", + "coming_soon": "Demnächst", + "on_now": "Jetzt", + "shows": "Shows", + "movies": "Filme", + "sports": "Sport", + "for_kids": "Für Kinder", + "news": "Nachrichten" + }, + "jellyseerr": { + "confirm": "Bestätigen", + "cancel": "Abbrechen", + "yes": "Ja", + "whats_wrong": "Hast du Probleme?", + "issue_type": "Fehlerart", + "select_an_issue": "Wähle einen Fehlerart aus", + "types": "Arten", + "describe_the_issue": "(optional) Beschreibe das Problem", + "submit_button": "Absenden", + "report_issue_button": "Fehler melden", + "request_button": "Anfragen", + "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", + "failed_to_login": "Fehler beim Anmelden", + "cast": "Besetzung", + "details": "Details", + "status": "Status", + "original_title": "Original Titel", + "series_type": "Serien Typ", + "release_dates": "Veröffentlichungsdaten", + "first_air_date": "Erstausstrahlungsdatum", + "next_air_date": "Nächstes Ausstrahlungsdatum", + "revenue": "Einnahmen", + "budget": "Budget", + "original_language": "Originalsprache", + "production_country": "Produktionsland", + "studios": "Studios", + "network": "Netzwerk", + "currently_streaming_on": "Derzeit im Streaming auf", + "advanced": "Erweitert", + "request_as": "Anfragen als", + "tags": "Tags", + "quality_profile": "Qualitätsprofil", + "root_folder": "Root-Ordner", + "season_all": "Season (all)", + "season_number": "Staffel {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Geboren", + "appearances": "Auftritte", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", + "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", + "issue_submitted": "Problem eingereicht!", + "requested_item": "{{item}} angefragt!", + "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", + "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" + } + }, + "tabs": { + "home": "Startseite", + "search": "Suche", + "library": "Bibliothek", + "custom_links": "Benutzerdefinierte Links", + "favorites": "Favoriten" + } +} diff --git a/translations/en.json b/translations/en.json index d8d18b72..30d7d466 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,477 +1,477 @@ { - "login": { - "username_required": "Username is required", - "error_title": "Error", - "login_title": "Log in", - "login_to_title": "Log in to", - "username_placeholder": "Username", - "password_placeholder": "Password", - "login_button": "Log in", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Enter code {{code}} to login", - "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", - "got_it": "Got it", - "connection_failed": "Connection failed", - "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", - "an_unexpected_error_occured": "An unexpected error occurred", - "change_server": "Change server", - "invalid_username_or_password": "Invalid username or password", - "user_does_not_have_permission_to_log_in": "User does not have permission to log in", - "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", - "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", - "there_is_a_server_error": "There is a server error", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" - }, - "server": { - "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "Connect", - "previous_servers": "previous servers", - "clear_button": "Clear", - "search_for_local_servers": "Search for local servers", - "searching": "Searching...", - "servers": "Servers" - }, - "home": { - "no_internet": "No Internet", - "no_items": "No items", - "no_internet_message": "No worries, you can still watch\ndownloaded content.", - "go_to_downloads": "Go to downloads", - "oops": "Oops!", - "error_message": "Something went wrong.\nPlease log out and in again.", - "continue_watching": "Continue Watching", - "next_up": "Next Up", - "recently_added_in": "Recently Added in {{libraryName}}", - "suggested_movies": "Suggested Movies", - "suggested_episodes": "Suggested Episodes", - "intro": { - "welcome_to_streamyfin": "Welcome to Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", - "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", - "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", - "centralised_settings_plugin_title": "Centralised Settings Plugin", - "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", - "done_button": "Done", - "go_to_settings_button": "Go to settings", - "read_more": "Read more" - }, - "settings": { - "settings_title": "Settings", - "log_out_button": "Log out", - "user_info": { - "user_info_title": "User Info", - "user": "User", - "server": "Server", - "token": "Token", - "app_version": "App Version" - }, - "quick_connect": { - "quick_connect_title": "Quick Connect", - "authorize_button": "Authorize Quick Connect", - "enter_the_quick_connect_code": "Enter the quick connect code...", - "success": "Success", - "quick_connect_autorized": "Quick Connect authorized", - "error": "Error", - "invalid_code": "Invalid code", - "authorize": "Authorize" - }, - "media_controls": { - "media_controls_title": "Media Controls", - "forward_skip_length": "Forward skip length", - "rewind_length": "Rewind length", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Set Audio Track From Previous Item", - "audio_language": "Audio language", - "audio_hint": "Choose a default audio language.", - "none": "None", - "language": "Language" - }, - "subtitles": { - "subtitle_title": "Subtitles", - "subtitle_language": "Subtitle language", - "subtitle_mode": "Subtitle Mode", - "set_subtitle_track": "Set Subtitle Track From Previous Item", - "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", - "none": "None", - "language": "Language", - "loading": "Loading", - "modes": { - "Default": "Default", - "Smart": "Smart", - "Always": "Always", - "None": "None", - "OnlyForced": "OnlyForced" - } - }, - "other": { - "other_title": "Other", - "follow_device_orientation": "Auto rotate", - "video_orientation": "Video orientation", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Default", - "ALL": "All", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Up", - "PORTRAIT_DOWN": "Portrait Down", - "LANDSCAPE": "Landscape", - "LANDSCAPE_LEFT": "Landscape Left", - "LANDSCAPE_RIGHT": "Landscape Right", - "OTHER": "Other", - "UNKNOWN": "Unknown" - }, - "safe_area_in_controls": "Safe area in controls", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Show Custom Menu Links", - "hide_libraries": "Hide Libraries", - "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", - "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download method", - "remux_max_download": "Remux max download", - "auto_download": "Auto download", - "optimized_versions_server": "Optimized versions server", - "save_button": "Save", - "optimized_server": "Optimized Server", - "optimized": "Optimized", - "default": "Default", - "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", - "read_more_about_optimized_server": "Read more about the optimize server.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", - "server_url": "Server URL", - "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Password", - "password_placeholder": "Enter password for Jellyfin user {{username}}", - "save_button": "Save", - "clear_button": "Clear", - "login_button": "Login", - "total_media_requests": "Total media requests", - "movie_quota_limit": "Movie quota limit", - "movie_quota_days": "Movie quota days", - "tv_quota_limit": "TV quota limit", - "tv_quota_days": "TV quota days", - "reset_jellyseerr_config_button": "Reset Jellyseerr config", - "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Enable Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", - "read_more_about_marlin": "Read more about Marlin.", - "save_button": "Save", - "toasts": { - "saved": "Saved" - } - } - }, - "storage": { - "storage_title": "Storage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Device {{availableSpace}}%", - "size_used": "{{used}} of {{total}} used", - "delete_all_downloaded_files": "Delete All Downloaded Files" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "No logs available", - "delete_all_logs": "Delete all logs" - }, - "languages": { - "title": "Languages", - "app_language": "App language", - "app_language_description": "Select the language for the app.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Error deleting files", - "background_downloads_enabled": "Background downloads enabled", - "background_downloads_disabled": "Background downloads disabled", - "connected": "Connected", - "could_not_connect": "Could not connect", - "invalid_url": "Invalid URL" - } - }, - "sessions": { - "title": "Sessions", - "no_active_sessions": "No active sessions" - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Series", - "movies": "Movies", - "queue": "Queue", - "queue_hint": "Queue and downloads will be lost on app restart", - "no_items_in_queue": "No items in queue", - "no_downloaded_items": "No downloaded items", - "delete_all_movies_button": "Delete all Movies", - "delete_all_tvseries_button": "Delete all TV-Series", - "delete_all_button": "Delete all", - "active_download": "Active download", - "no_active_downloads": "No active downloads", - "active_downloads": "Active downloads", - "new_app_version_requires_re_download": "New app version requires re-download", - "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", - "back": "Back", - "delete": "Delete", - "something_went_wrong": "Something went wrong", - "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methods", - "toasts": { - "you_are_not_allowed_to_download_files": "You are not allowed to download files.", - "deleted_all_movies_successfully": "Deleted all movies successfully!", - "failed_to_delete_all_movies": "Failed to delete all movies", - "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", - "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", - "download_cancelled": "Download cancelled", - "could_not_cancel_download": "Could not cancel download", - "download_completed": "Download completed", - "download_started_for": "Download started for {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", - "download_stated_for_item": "Download started for {{item}}", - "download_failed_for_item": "Download failed for {{item}} - {{error}}", - "download_completed_for_item": "Download completed for {{item}}", - "queued_item_for_optimization": "Queued {{item}} for optimization", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", - "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", - "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", - "go_to_downloads": "Go to downloads" - } - } - }, - "search": { - "search_here": "Search here...", - "search": "Search...", - "x_items": "{{count}} items", - "library": "Library", - "discover": "Discover", - "no_results": "No results", - "no_results_found_for": "No results found for", - "movies": "Movies", - "series": "Series", - "episodes": "Episodes", - "collections": "Collections", - "actors": "Actors", - "request_movies": "Request Movies", - "request_series": "Request Series", - "recently_added": "Recently Added", - "recent_requests": "Recent Requests", - "plex_watchlist": "Plex Watchlist", - "trending": "Trending", - "popular_movies": "Popular Movies", - "movie_genres": "Movie Genres", - "upcoming_movies": "Upcoming Movies", - "studios": "Studios", - "popular_tv": "Popular TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Upcoming TV", - "networks": "Networks", - "tmdb_movie_keyword": "TMDB Movie Keyword", - "tmdb_movie_genre": "TMDB Movie Genre", - "tmdb_tv_keyword": "TMDB TV Keyword", - "tmdb_tv_genre": "TMDB TV Genre", - "tmdb_search": "TMDB Search", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", - "tmdb_tv_streaming_services": "TMDB TV Streaming Services" - }, - "library": { - "no_items_found": "No items found", - "no_results": "No results", - "no_libraries_found": "No libraries found", - "item_types": { - "movies": "movies", - "series": "series", - "boxsets": "box sets", - "items": "items" - }, - "options": { - "display": "Display", - "row": "Row", - "list": "List", - "image_style": "Image style", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Show titles", - "show_stats": "Show stats" - }, - "filters": { - "genres": "Genres", - "years": "Years", - "sort_by": "Sort By", - "sort_order": "Sort Order", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Series", - "movies": "Movies", - "episodes": "Episodes", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists", - "noDataTitle": "No favorites yet", - "noData": "Mark items as favorites to see them appear here for quick access." - }, - "custom_links": { - "no_links": "No links" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Failed to get the stream URL", - "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", - "client_error": "Client error", - "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", - "message_from_server": "Message from server: {{message}}", - "video_has_finished_playing": "Video has finished playing!", - "no_video_source": "No video source...", - "next_episode": "Next Episode", - "refresh_tracks": "Refresh Tracks", - "subtitle_tracks": "Subtitle Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Playback State:", - "no_data_available": "No data available", - "index": "Index:" - }, - "item_card": { - "next_up": "Next up", - "no_items_to_display": "No items to display", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seasons", - "season": "Season", - "no_episodes_for_this_season": "No episodes for this season", - "overview": "Overview", - "more_with": "More with {{name}}", - "similar_items": "Similar items", - "no_similar_items_found": "No similar items found", - "video": "Video", - "more_details": "More details", - "quality": "Quality", - "audio": "Audio", - "subtitles": "Subtitle", - "show_more": "Show more", - "show_less": "Show less", - "appeared_in": "Appeared in", - "could_not_load_item": "Could not load item", - "none": "None", - "download": { - "download_season": "Download Season", - "download_series": "Download Series", - "download_episode": "Download Episode", - "download_movie": "Download Movie", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Using optimized server", - "using_default_method": "Using default method" - } - }, - "live_tv": { - "next": "Next", - "previous": "Previous", - "live_tv": "Live TV", - "coming_soon": "Coming soon", - "on_now": "On now", - "shows": "Shows", - "movies": "Movies", - "sports": "Sports", - "for_kids": "For Kids", - "news": "News" - }, - "jellyseerr": { - "confirm": "Confirm", - "cancel": "Cancel", - "yes": "Yes", - "whats_wrong": "What's wrong?", - "issue_type": "Issue type", - "select_an_issue": "Select an issue", - "types": "Types", - "describe_the_issue": "(optional) Describe the issue...", - "submit_button": "Submit", - "report_issue_button": "Report issue", - "request_button": "Request", - "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", - "failed_to_login": "Failed to login", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Original Title", - "series_type": "Series Type", - "release_dates": "Release Dates", - "first_air_date": "First Air Date", - "next_air_date": "Next Air Date", - "revenue": "Revenue", - "budget": "Budget", - "original_language": "Original Language", - "production_country": "Production Country", - "studios": "Studios", - "network": "Network", - "currently_streaming_on": "Currently Streaming on", - "advanced": "Advanced", - "request_as": "Request As", - "tags": "Tags", - "quality_profile": "Quality Profile", - "root_folder": "Root Folder", - "season_all": "Season (all)", - "season_number": "Season {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Born", - "appearances": "Appearances", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", - "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", - "issue_submitted": "Issue submitted!", - "requested_item": "Requested {{item}}!", - "you_dont_have_permission_to_request": "You don't have permission to request!", - "something_went_wrong_requesting_media": "Something went wrong requesting media!" - } - }, - "tabs": { - "home": "Home", - "search": "Search", - "library": "Library", - "custom_links": "Custom Links", - "favorites": "Favorites" - } + "login": { + "username_required": "Username is required", + "error_title": "Error", + "login_title": "Log in", + "login_to_title": "Log in to", + "username_placeholder": "Username", + "password_placeholder": "Password", + "login_button": "Log in", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Enter code {{code}} to login", + "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", + "got_it": "Got it", + "connection_failed": "Connection failed", + "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", + "an_unexpected_error_occured": "An unexpected error occurred", + "change_server": "Change server", + "invalid_username_or_password": "Invalid username or password", + "user_does_not_have_permission_to_log_in": "User does not have permission to log in", + "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", + "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", + "there_is_a_server_error": "There is a server error", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" + }, + "server": { + "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "Connect", + "previous_servers": "previous servers", + "clear_button": "Clear", + "search_for_local_servers": "Search for local servers", + "searching": "Searching...", + "servers": "Servers" + }, + "home": { + "no_internet": "No Internet", + "no_items": "No items", + "no_internet_message": "No worries, you can still watch\ndownloaded content.", + "go_to_downloads": "Go to downloads", + "oops": "Oops!", + "error_message": "Something went wrong.\nPlease log out and in again.", + "continue_watching": "Continue Watching", + "next_up": "Next Up", + "recently_added_in": "Recently Added in {{libraryName}}", + "suggested_movies": "Suggested Movies", + "suggested_episodes": "Suggested Episodes", + "intro": { + "welcome_to_streamyfin": "Welcome to Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", + "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", + "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", + "centralised_settings_plugin_title": "Centralised Settings Plugin", + "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", + "done_button": "Done", + "go_to_settings_button": "Go to settings", + "read_more": "Read more" + }, + "settings": { + "settings_title": "Settings", + "log_out_button": "Log out", + "user_info": { + "user_info_title": "User Info", + "user": "User", + "server": "Server", + "token": "Token", + "app_version": "App Version" + }, + "quick_connect": { + "quick_connect_title": "Quick Connect", + "authorize_button": "Authorize Quick Connect", + "enter_the_quick_connect_code": "Enter the quick connect code...", + "success": "Success", + "quick_connect_autorized": "Quick Connect authorized", + "error": "Error", + "invalid_code": "Invalid code", + "authorize": "Authorize" + }, + "media_controls": { + "media_controls_title": "Media Controls", + "forward_skip_length": "Forward skip length", + "rewind_length": "Rewind length", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Set Audio Track From Previous Item", + "audio_language": "Audio language", + "audio_hint": "Choose a default audio language.", + "none": "None", + "language": "Language" + }, + "subtitles": { + "subtitle_title": "Subtitles", + "subtitle_language": "Subtitle language", + "subtitle_mode": "Subtitle Mode", + "set_subtitle_track": "Set Subtitle Track From Previous Item", + "subtitle_size": "Subtitle Size", + "subtitle_hint": "Configure subtitle preference.", + "none": "None", + "language": "Language", + "loading": "Loading", + "modes": { + "Default": "Default", + "Smart": "Smart", + "Always": "Always", + "None": "None", + "OnlyForced": "OnlyForced" + } + }, + "other": { + "other_title": "Other", + "follow_device_orientation": "Auto rotate", + "video_orientation": "Video orientation", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Default", + "ALL": "All", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Up", + "PORTRAIT_DOWN": "Portrait Down", + "LANDSCAPE": "Landscape", + "LANDSCAPE_LEFT": "Landscape Left", + "LANDSCAPE_RIGHT": "Landscape Right", + "OTHER": "Other", + "UNKNOWN": "Unknown" + }, + "safe_area_in_controls": "Safe area in controls", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Show Custom Menu Links", + "hide_libraries": "Hide Libraries", + "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", + "disable_haptic_feedback": "Disable Haptic Feedback", + "default_quality": "Default quality" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download method", + "remux_max_download": "Remux max download", + "auto_download": "Auto download", + "optimized_versions_server": "Optimized versions server", + "save_button": "Save", + "optimized_server": "Optimized Server", + "optimized": "Optimized", + "default": "Default", + "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", + "read_more_about_optimized_server": "Read more about the optimize server.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", + "server_url": "Server URL", + "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Password", + "password_placeholder": "Enter password for Jellyfin user {{username}}", + "save_button": "Save", + "clear_button": "Clear", + "login_button": "Login", + "total_media_requests": "Total media requests", + "movie_quota_limit": "Movie quota limit", + "movie_quota_days": "Movie quota days", + "tv_quota_limit": "TV quota limit", + "tv_quota_days": "TV quota days", + "reset_jellyseerr_config_button": "Reset Jellyseerr config", + "unlimited": "Unlimited", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Enable Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", + "read_more_about_marlin": "Read more about Marlin.", + "save_button": "Save", + "toasts": { + "saved": "Saved" + } + } + }, + "storage": { + "storage_title": "Storage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Device {{availableSpace}}%", + "size_used": "{{used}} of {{total}} used", + "delete_all_downloaded_files": "Delete All Downloaded Files" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "No logs available", + "delete_all_logs": "Delete all logs" + }, + "languages": { + "title": "Languages", + "app_language": "App language", + "app_language_description": "Select the language for the app.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Error deleting files", + "background_downloads_enabled": "Background downloads enabled", + "background_downloads_disabled": "Background downloads disabled", + "connected": "Connected", + "could_not_connect": "Could not connect", + "invalid_url": "Invalid URL" + } + }, + "sessions": { + "title": "Sessions", + "no_active_sessions": "No active sessions" + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Series", + "movies": "Movies", + "queue": "Queue", + "queue_hint": "Queue and downloads will be lost on app restart", + "no_items_in_queue": "No items in queue", + "no_downloaded_items": "No downloaded items", + "delete_all_movies_button": "Delete all Movies", + "delete_all_tvseries_button": "Delete all TV-Series", + "delete_all_button": "Delete all", + "active_download": "Active download", + "no_active_downloads": "No active downloads", + "active_downloads": "Active downloads", + "new_app_version_requires_re_download": "New app version requires re-download", + "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", + "back": "Back", + "delete": "Delete", + "something_went_wrong": "Something went wrong", + "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methods", + "toasts": { + "you_are_not_allowed_to_download_files": "You are not allowed to download files.", + "deleted_all_movies_successfully": "Deleted all movies successfully!", + "failed_to_delete_all_movies": "Failed to delete all movies", + "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", + "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", + "download_cancelled": "Download cancelled", + "could_not_cancel_download": "Could not cancel download", + "download_completed": "Download completed", + "download_started_for": "Download started for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", + "download_stated_for_item": "Download started for {{item}}", + "download_failed_for_item": "Download failed for {{item}} - {{error}}", + "download_completed_for_item": "Download completed for {{item}}", + "queued_item_for_optimization": "Queued {{item}} for optimization", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", + "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", + "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", + "go_to_downloads": "Go to downloads" + } + } + }, + "search": { + "search_here": "Search here...", + "search": "Search...", + "x_items": "{{count}} items", + "library": "Library", + "discover": "Discover", + "no_results": "No results", + "no_results_found_for": "No results found for", + "movies": "Movies", + "series": "Series", + "episodes": "Episodes", + "collections": "Collections", + "actors": "Actors", + "request_movies": "Request Movies", + "request_series": "Request Series", + "recently_added": "Recently Added", + "recent_requests": "Recent Requests", + "plex_watchlist": "Plex Watchlist", + "trending": "Trending", + "popular_movies": "Popular Movies", + "movie_genres": "Movie Genres", + "upcoming_movies": "Upcoming Movies", + "studios": "Studios", + "popular_tv": "Popular TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Upcoming TV", + "networks": "Networks", + "tmdb_movie_keyword": "TMDB Movie Keyword", + "tmdb_movie_genre": "TMDB Movie Genre", + "tmdb_tv_keyword": "TMDB TV Keyword", + "tmdb_tv_genre": "TMDB TV Genre", + "tmdb_search": "TMDB Search", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", + "tmdb_tv_streaming_services": "TMDB TV Streaming Services" + }, + "library": { + "no_items_found": "No items found", + "no_results": "No results", + "no_libraries_found": "No libraries found", + "item_types": { + "movies": "movies", + "series": "series", + "boxsets": "box sets", + "items": "items" + }, + "options": { + "display": "Display", + "row": "Row", + "list": "List", + "image_style": "Image style", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Show titles", + "show_stats": "Show stats" + }, + "filters": { + "genres": "Genres", + "years": "Years", + "sort_by": "Sort By", + "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Series", + "movies": "Movies", + "episodes": "Episodes", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "No favorites yet", + "noData": "Mark items as favorites to see them appear here for quick access." + }, + "custom_links": { + "no_links": "No links" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Failed to get the stream URL", + "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", + "client_error": "Client error", + "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", + "message_from_server": "Message from server: {{message}}", + "video_has_finished_playing": "Video has finished playing!", + "no_video_source": "No video source...", + "next_episode": "Next Episode", + "refresh_tracks": "Refresh Tracks", + "subtitle_tracks": "Subtitle Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Playback State:", + "no_data_available": "No data available", + "index": "Index:" + }, + "item_card": { + "next_up": "Next up", + "no_items_to_display": "No items to display", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seasons", + "season": "Season", + "no_episodes_for_this_season": "No episodes for this season", + "overview": "Overview", + "more_with": "More with {{name}}", + "similar_items": "Similar items", + "no_similar_items_found": "No similar items found", + "video": "Video", + "more_details": "More details", + "quality": "Quality", + "audio": "Audio", + "subtitles": "Subtitle", + "show_more": "Show more", + "show_less": "Show less", + "appeared_in": "Appeared in", + "could_not_load_item": "Could not load item", + "none": "None", + "download": { + "download_season": "Download Season", + "download_series": "Download Series", + "download_episode": "Download Episode", + "download_movie": "Download Movie", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Using optimized server", + "using_default_method": "Using default method" + } + }, + "live_tv": { + "next": "Next", + "previous": "Previous", + "live_tv": "Live TV", + "coming_soon": "Coming soon", + "on_now": "On now", + "shows": "Shows", + "movies": "Movies", + "sports": "Sports", + "for_kids": "For Kids", + "news": "News" + }, + "jellyseerr": { + "confirm": "Confirm", + "cancel": "Cancel", + "yes": "Yes", + "whats_wrong": "What's wrong?", + "issue_type": "Issue type", + "select_an_issue": "Select an issue", + "types": "Types", + "describe_the_issue": "(optional) Describe the issue...", + "submit_button": "Submit", + "report_issue_button": "Report issue", + "request_button": "Request", + "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", + "failed_to_login": "Failed to login", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Original Title", + "series_type": "Series Type", + "release_dates": "Release Dates", + "first_air_date": "First Air Date", + "next_air_date": "Next Air Date", + "revenue": "Revenue", + "budget": "Budget", + "original_language": "Original Language", + "production_country": "Production Country", + "studios": "Studios", + "network": "Network", + "currently_streaming_on": "Currently Streaming on", + "advanced": "Advanced", + "request_as": "Request As", + "tags": "Tags", + "quality_profile": "Quality Profile", + "root_folder": "Root Folder", + "season_all": "Season (all)", + "season_number": "Season {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Born", + "appearances": "Appearances", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", + "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", + "issue_submitted": "Issue submitted!", + "requested_item": "Requested {{item}}!", + "you_dont_have_permission_to_request": "You don't have permission to request!", + "something_went_wrong_requesting_media": "Something went wrong requesting media!" + } + }, + "tabs": { + "home": "Home", + "search": "Search", + "library": "Library", + "custom_links": "Custom Links", + "favorites": "Favorites" + } } diff --git a/translations/es.json b/translations/es.json index 5182c31f..0745aad3 100644 --- a/translations/es.json +++ b/translations/es.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Se requiere un nombre de usuario", - "error_title": "Error", - "login_title": "Iniciar sesión", - "login_to_title": "Iniciar sesión en", - "username_placeholder": "Nombre de usuario", - "password_placeholder": "Contraseña", - "login_button": "Iniciar sesión", - "quick_connect": "Conexión rápida", - "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", - "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", - "got_it": "Entendido", - "connection_failed": "Conexión fallida", - "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", - "an_unexpected_error_occured": "Ha ocurrido un error inesperado", - "change_server": "Cambiar servidor", - "invalid_username_or_password": "Usuario o contraseña inválidos", - "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", - "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", - "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", - "there_is_a_server_error": "Hay un error en el servidor", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" - }, - "server": { - "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", - "server_url_placeholder": "http(s)://tu-servidor.com", - "connect_button": "Conectar", - "previous_servers": "Servidores previos", - "clear_button": "Limpiar", - "search_for_local_servers": "Buscar servidores locales", - "searching": "Buscando...", - "servers": "Servidores" - }, - "home": { - "no_internet": "Sin internet", - "no_items": "No hay ítems", - "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", - "go_to_downloads": "Ir a descargas", - "oops": "¡Vaya!", - "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", - "continue_watching": "Seguir viendo", - "next_up": "A continuación", - "recently_added_in": "Recientemente añadido en {{libraryName}}", - "suggested_movies": "Películas sugeridas", - "suggested_episodes": "Episodios sugeridos", - "intro": { - "welcome_to_streamyfin": "Bienvenido a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", - "features_title": "Características", - "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", - "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", - "downloads_feature_title": "Descargas", - "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", - "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", - "centralised_settings_plugin_title": "Plugin de configuración centralizada", - "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", - "done_button": "Hecho", - "go_to_settings_button": "Ir a la configuración", - "read_more": "Leer más" - }, - "settings": { - "settings_title": "Configuración", - "log_out_button": "Cerrar sesión", - "user_info": { - "user_info_title": "Información de usuario", - "user": "Usuario", - "server": "Servidor", - "token": "Token", - "app_version": "Versión de la app" - }, - "quick_connect": { - "quick_connect_title": "Conexión rápida", - "authorize_button": "Autorizar conexión rápida", - "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", - "success": "Hecho", - "quick_connect_autorized": "Conexión rápida autorizada", - "error": "Error", - "invalid_code": "Código inválido", - "authorize": "Autorizar" - }, - "media_controls": { - "media_controls_title": "Controles de reproducción", - "forward_skip_length": "Longitud de avance", - "rewind_length": "Longitud de retroceso", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Establecer pista del elemento anterior", - "audio_language": "Idioma de audio", - "audio_hint": "Elige un idioma de audio por defecto.", - "none": "Ninguno", - "language": "Idioma" - }, - "subtitles": { - "subtitle_title": "Subtítulos", - "subtitle_language": "Idioma de subtítulos", - "subtitle_mode": "Modo de subtítulos", - "set_subtitle_track": "Establecer pista del elemento anterior", - "subtitle_size": "Tamaño de subtítulos", - "subtitle_hint": "Configurar preferencias de subtítulos.", - "none": "Ninguno", - "language": "Idioma", - "loading": "Cargando", - "modes": { - "Default": "Por defecto", - "Smart": "Inteligente", - "Always": "Siempre", - "None": "Nada", - "OnlyForced": "Solo forzados" - } - }, - "other": { - "other_title": "Otros", - "follow_device_orientation": "Rotación automática", - "video_orientation": "Orientación de vídeo", - "orientation": "Orientación", - "orientations": { - "DEFAULT": "Por defecto", - "ALL": "Todas", - "PORTRAIT": "Vertical", - "PORTRAIT_UP": "Vertical arriba", - "PORTRAIT_DOWN": "Vertical abajo", - "LANDSCAPE": "Horizontal", - "LANDSCAPE_LEFT": "Horizontal izquierda", - "LANDSCAPE_RIGHT": "Horizontal derecha", - "OTHER": "Otra", - "UNKNOWN": "Desconocida" - }, - "safe_area_in_controls": "Área segura en controles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostrar enlaces de menú personalizados", - "hide_libraries": "Ocultar bibliotecas", - "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", - "disable_haptic_feedback": "Desactivar feedback háptico", - "default_quality": "Calidad por defecto" - }, - "downloads": { - "downloads_title": "Descargas", - "download_method": "Método de descarga", - "remux_max_download": "Remux máx. descarga", - "auto_download": "Descarga automática", - "optimized_versions_server": "Servidor de versiones optimizadas", - "save_button": "Guardar", - "optimized_server": "Servidor optimizado", - "optimized": "Optimizado", - "default": "Por defecto", - "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", - "server_url": "URL del servidor", - "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Contrasñea", - "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", - "save_button": "Guardar", - "clear_button": "Limpiar", - "login_button": "Iniciar sesión", - "total_media_requests": "Peticiones totales de medios", - "movie_quota_limit": "Límite de cuota de películas", - "movie_quota_days": "Días de cuota de películas", - "tv_quota_limit": "Límite de cuota de series", - "tv_quota_days": "Días de cuota de series", - "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", - "unlimited": "Ilimitado", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Habilitar búsqueda de Marlin", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto", - "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_marlin": "Leer más sobre Marlin.", - "save_button": "Guardar", - "toasts": { - "saved": "Guardado" - } - } - }, - "storage": { - "storage_title": "Almacenamiento", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} de {{total}} usado", - "delete_all_downloaded_files": "Eliminar todos los archivos descargados" - }, - "intro": { - "show_intro": "Mostrar intro", - "reset_intro": "Restablecer intro" - }, - "logs": { - "logs_title": "Registros", - "no_logs_available": "No hay registros disponibles", - "delete_all_logs": "Eliminar todos los registros" - }, - "languages": { - "title": "Idiomas", - "app_language": "Idioma de la app", - "app_language_description": "Selecciona el idioma de la app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Error al eliminar archivos", - "background_downloads_enabled": "Descargas en segundo plano habilitadas", - "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", - "connected": "Conectado", - "could_not_connect": "No se pudo conectar", - "invalid_url": "URL inválida" - } - }, - "downloads": { - "downloads_title": "Descargas", - "tvseries": "Series", - "movies": "Películas", - "queue": "Cola", - "queue_hint": "La cola de series y películas se perderá al reiniciar la app", - "no_items_in_queue": "No hay ítems en la cola", - "no_downloaded_items": "No hay ítems descargados", - "delete_all_movies_button": "Eliminar todas las películas", - "delete_all_tvseries_button": "Eliminar todas las series", - "delete_all_button": "Eliminar todo", - "active_download": "Descarga activa", - "no_active_downloads": "No hay descargas activas", - "active_downloads": "Descargas activas", - "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", - "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", - "back": "Atrás", - "delete": "Borrar", - "something_went_wrong": "Algo ha salido mal", - "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", - "eta": "{{eta}} restante", - "methods": "Métodos", - "toasts": { - "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", - "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", - "failed_to_delete_all_movies": "Error al eliminar todas las películas", - "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", - "failed_to_delete_all_tvseries": "Error al eliminar todas las series", - "download_cancelled": "Descarga cancelada", - "could_not_cancel_download": "No se pudo cancelar la descarga", - "download_completed": "Descarga completada", - "download_started_for": "Descarga iniciada para {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", - "download_stated_for_item": "Descarga iniciada para {{item}}", - "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", - "download_completed_for_item": "Descarga completada para {{item}}", - "queued_item_for_optimization": "{{item}} en cola para optimización", - "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", - "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", - "no_response_received_from_server": "No se ha recibido respuesta del servidor", - "error_setting_up_the_request": "Error al configurar la petición", - "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", - "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", - "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", - "go_to_downloads": "Ir a descargas" - } - } - }, - "search": { - "search_here": "Buscar aquí...", - "search": "Buscar...", - "x_items": "{{count}} ítems", - "library": "Biblioteca", - "discover": "Descubrir", - "no_results": "Sin resultados", - "no_results_found_for": "No se han encontrado resultados para", - "movies": "Películas", - "series": "Series", - "episodes": "Episodios", - "collections": "Colecciones", - "actors": "Actores", - "request_movies": "Solicitar películas", - "request_series": "Solicitar series", - "recently_added": "Recientemente añadido", - "recent_requests": "Solicitudes recientes", - "plex_watchlist": "Lista de seguimiento de Plex", - "trending": "Trending", - "popular_movies": "Películas populares", - "movie_genres": "Géneros de películas", - "upcoming_movies": "Próximas películas", - "studios": "Estudios", - "popular_tv": "Series populares", - "tv_genres": "Géneros de series", - "upcoming_tv": "Próximas series", - "networks": "Cadenas", - "tmdb_movie_keyword": "Palabra clave de película de TMDB", - "tmdb_movie_genre": "Género de película de TMDB", - "tmdb_tv_keyword": "Palabra clave de serie de TMDB", - "tmdb_tv_genre": "Género de serie de TMDB", - "tmdb_search": "Búsqueda de TMDB", - "tmdb_studio": "Estudio de TMDB", - "tmdb_network": "Cadena de TMDB", - "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", - "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" - }, - "library": { - "no_items_found": "No se han encontrado ítems", - "no_results": "Sin resultados", - "no_libraries_found": "No se han encontrado bibliotecas", - "item_types": { - "movies": "películas", - "series": "series", - "boxsets": "colecciones", - "items": "ítems" - }, - "options": { - "display": "Mostrar", - "row": "Fila", - "list": "Lista", - "image_style": "Estilo de imagen", - "poster": "Poster", - "cover": "Portada", - "show_titles": "Mostrar títulos", - "show_stats": "Mostrar estadísticas" - }, - "filters": { - "genres": "Géneros", - "years": "Años", - "sort_by": "Ordenar por", - "sort_order": "Ordenar", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiquetas" - } - }, - "favorites": { - "series": "Series", - "movies": "Películas", - "episodes": "Episodios", - "videos": "Vídeos", - "boxsets": "Colecciones", - "playlists": "Playlists", - "noDataTitle": "Aún no hay favoritos", - "noData": "Marca elementos como favoritos para verlos aparecer aquí para un acceso rápido." - }, - "custom_links": { - "no_links": "Sin enlaces" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Error al obtener la URL del stream", - "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", - "client_error": "Error del cliente", - "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", - "message_from_server": "Mensaje del servidor: {{message}}", - "video_has_finished_playing": "El vídeo ha terminado de reproducirse", - "no_video_source": "No hay fuente de vídeo...", - "next_episode": "Siguiente episodio", - "refresh_tracks": "Refrescar pistas", - "subtitle_tracks": "Pistas de subtítulos:", - "audio_tracks": "Pistas de audio:", - "playback_state": "Estado de la reproducción:", - "no_data_available": "No hay datos disponibles", - "index": "Índice:" - }, - "item_card": { - "next_up": "A continuación", - "no_items_to_display": "No hay ítems para mostrar", - "cast_and_crew": "Reparto y equipo", - "series": "Series", - "seasons": "Temporadas", - "season": "Temporada", - "no_episodes_for_this_season": "No hay episodios para esta temporada", - "overview": "Resumen", - "more_with": "Más con {{name}}", - "similar_items": "Ítems similares", - "no_similar_items_found": "No se han encontrado ítems similares", - "video": "Vídeo", - "more_details": "Más detalles", - "quality": "Calidad", - "audio": "Audio", - "subtitles": "Subtítulos", - "show_more": "Mostrar más", - "show_less": "Mostrar menos", - "appeared_in": "Apareció en", - "could_not_load_item": "No se pudo cargar el ítem", - "none": "Ninguno", - "download": { - "download_season": "Descargar temporada", - "download_series": "Descargar serie", - "download_episode": "Descargar episodio", - "download_movie": "Descargar película", - "download_x_item": "Descargar {{item_count}} ítems", - "download_button": "Descargar", - "using_optimized_server": "Usando servidor optimizado", - "using_default_method": "Usando método por defecto" - } - }, - "live_tv": { - "next": "Siguiente", - "previous": "Anterior", - "live_tv": "TV en directo", - "coming_soon": "Próximamente", - "on_now": "En directo", - "shows": "Programas", - "movies": "Películas", - "sports": "Deportes", - "for_kids": "Para niños", - "news": "Noticias" - }, - "jellyseerr": { - "confirm": "Confirmar", - "cancel": "Cancelar", - "yes": "Sí", - "whats_wrong": "¿Qué pasa?", - "issue_type": "Tipo de problema", - "select_an_issue": "Selecciona un problema", - "types": "Tipos", - "describe_the_issue": "(opcional) Describe el problema...", - "submit_button": "Enviar", - "report_issue_button": "Reportar problema", - "request_button": "Solicitar", - "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", - "failed_to_login": "Error al iniciar sesión", - "cast": "Reparto", - "details": "Detalles", - "status": "Estado", - "original_title": "Título original", - "series_type": "Tipo de serie", - "release_dates": "Fechas de estreno", - "first_air_date": "Primera fecha de emisión", - "next_air_date": "Próxima fecha de emisión", - "revenue": "Ingresos", - "budget": "Presupuesto", - "original_language": "Idioma original", - "production_country": "País de producción", - "studios": "Estudios", - "network": "Cadena", - "currently_streaming_on": "Actualmente en streaming en", - "advanced": "Avanzado", - "request_as": "Solicitar como", - "tags": "Etiquetas", - "quality_profile": "Perfil de calidad", - "root_folder": "Carpeta raíz", - "season_all": "Season (all)", - "season_number": "Temporada {{season_number}}", - "number_episodes": "{{episode_number}} episodios", - "born": "Nacido", - "appearances": "Apariciones", - "toasts": { - "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", - "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", - "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", - "issue_submitted": "¡Problema enviado!", - "requested_item": "¡{{item}} solicitado!", - "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", - "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" - } - }, - "tabs": { - "home": "Inicio", - "search": "Buscar", - "library": "Bibliotecas", - "custom_links": "Enlaces personalizados", - "favorites": "Favoritos" - } + "login": { + "username_required": "Se requiere un nombre de usuario", + "error_title": "Error", + "login_title": "Iniciar sesión", + "login_to_title": "Iniciar sesión en", + "username_placeholder": "Nombre de usuario", + "password_placeholder": "Contraseña", + "login_button": "Iniciar sesión", + "quick_connect": "Conexión rápida", + "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", + "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", + "got_it": "Entendido", + "connection_failed": "Conexión fallida", + "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", + "an_unexpected_error_occured": "Ha ocurrido un error inesperado", + "change_server": "Cambiar servidor", + "invalid_username_or_password": "Usuario o contraseña inválidos", + "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", + "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", + "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", + "there_is_a_server_error": "Hay un error en el servidor", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" + }, + "server": { + "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", + "server_url_placeholder": "http(s)://tu-servidor.com", + "connect_button": "Conectar", + "previous_servers": "Servidores previos", + "clear_button": "Limpiar", + "search_for_local_servers": "Buscar servidores locales", + "searching": "Buscando...", + "servers": "Servidores" + }, + "home": { + "no_internet": "Sin internet", + "no_items": "No hay ítems", + "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", + "go_to_downloads": "Ir a descargas", + "oops": "¡Vaya!", + "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", + "continue_watching": "Seguir viendo", + "next_up": "A continuación", + "recently_added_in": "Recientemente añadido en {{libraryName}}", + "suggested_movies": "Películas sugeridas", + "suggested_episodes": "Episodios sugeridos", + "intro": { + "welcome_to_streamyfin": "Bienvenido a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", + "features_title": "Características", + "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", + "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", + "downloads_feature_title": "Descargas", + "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", + "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", + "centralised_settings_plugin_title": "Plugin de configuración centralizada", + "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", + "done_button": "Hecho", + "go_to_settings_button": "Ir a la configuración", + "read_more": "Leer más" + }, + "settings": { + "settings_title": "Configuración", + "log_out_button": "Cerrar sesión", + "user_info": { + "user_info_title": "Información de usuario", + "user": "Usuario", + "server": "Servidor", + "token": "Token", + "app_version": "Versión de la app" + }, + "quick_connect": { + "quick_connect_title": "Conexión rápida", + "authorize_button": "Autorizar conexión rápida", + "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", + "success": "Hecho", + "quick_connect_autorized": "Conexión rápida autorizada", + "error": "Error", + "invalid_code": "Código inválido", + "authorize": "Autorizar" + }, + "media_controls": { + "media_controls_title": "Controles de reproducción", + "forward_skip_length": "Longitud de avance", + "rewind_length": "Longitud de retroceso", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Establecer pista del elemento anterior", + "audio_language": "Idioma de audio", + "audio_hint": "Elige un idioma de audio por defecto.", + "none": "Ninguno", + "language": "Idioma" + }, + "subtitles": { + "subtitle_title": "Subtítulos", + "subtitle_language": "Idioma de subtítulos", + "subtitle_mode": "Modo de subtítulos", + "set_subtitle_track": "Establecer pista del elemento anterior", + "subtitle_size": "Tamaño de subtítulos", + "subtitle_hint": "Configurar preferencias de subtítulos.", + "none": "Ninguno", + "language": "Idioma", + "loading": "Cargando", + "modes": { + "Default": "Por defecto", + "Smart": "Inteligente", + "Always": "Siempre", + "None": "Nada", + "OnlyForced": "Solo forzados" + } + }, + "other": { + "other_title": "Otros", + "follow_device_orientation": "Rotación automática", + "video_orientation": "Orientación de vídeo", + "orientation": "Orientación", + "orientations": { + "DEFAULT": "Por defecto", + "ALL": "Todas", + "PORTRAIT": "Vertical", + "PORTRAIT_UP": "Vertical arriba", + "PORTRAIT_DOWN": "Vertical abajo", + "LANDSCAPE": "Horizontal", + "LANDSCAPE_LEFT": "Horizontal izquierda", + "LANDSCAPE_RIGHT": "Horizontal derecha", + "OTHER": "Otra", + "UNKNOWN": "Desconocida" + }, + "safe_area_in_controls": "Área segura en controles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostrar enlaces de menú personalizados", + "hide_libraries": "Ocultar bibliotecas", + "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", + "disable_haptic_feedback": "Desactivar feedback háptico", + "default_quality": "Calidad por defecto" + }, + "downloads": { + "downloads_title": "Descargas", + "download_method": "Método de descarga", + "remux_max_download": "Remux máx. descarga", + "auto_download": "Descarga automática", + "optimized_versions_server": "Servidor de versiones optimizadas", + "save_button": "Guardar", + "optimized_server": "Servidor optimizado", + "optimized": "Optimizado", + "default": "Por defecto", + "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", + "server_url": "URL del servidor", + "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Contrasñea", + "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", + "save_button": "Guardar", + "clear_button": "Limpiar", + "login_button": "Iniciar sesión", + "total_media_requests": "Peticiones totales de medios", + "movie_quota_limit": "Límite de cuota de películas", + "movie_quota_days": "Días de cuota de películas", + "tv_quota_limit": "Límite de cuota de series", + "tv_quota_days": "Días de cuota de series", + "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", + "unlimited": "Ilimitado", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Habilitar búsqueda de Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto", + "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_marlin": "Leer más sobre Marlin.", + "save_button": "Guardar", + "toasts": { + "saved": "Guardado" + } + } + }, + "storage": { + "storage_title": "Almacenamiento", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} de {{total}} usado", + "delete_all_downloaded_files": "Eliminar todos los archivos descargados" + }, + "intro": { + "show_intro": "Mostrar intro", + "reset_intro": "Restablecer intro" + }, + "logs": { + "logs_title": "Registros", + "no_logs_available": "No hay registros disponibles", + "delete_all_logs": "Eliminar todos los registros" + }, + "languages": { + "title": "Idiomas", + "app_language": "Idioma de la app", + "app_language_description": "Selecciona el idioma de la app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Error al eliminar archivos", + "background_downloads_enabled": "Descargas en segundo plano habilitadas", + "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", + "connected": "Conectado", + "could_not_connect": "No se pudo conectar", + "invalid_url": "URL inválida" + } + }, + "downloads": { + "downloads_title": "Descargas", + "tvseries": "Series", + "movies": "Películas", + "queue": "Cola", + "queue_hint": "La cola de series y películas se perderá al reiniciar la app", + "no_items_in_queue": "No hay ítems en la cola", + "no_downloaded_items": "No hay ítems descargados", + "delete_all_movies_button": "Eliminar todas las películas", + "delete_all_tvseries_button": "Eliminar todas las series", + "delete_all_button": "Eliminar todo", + "active_download": "Descarga activa", + "no_active_downloads": "No hay descargas activas", + "active_downloads": "Descargas activas", + "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", + "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", + "back": "Atrás", + "delete": "Borrar", + "something_went_wrong": "Algo ha salido mal", + "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", + "eta": "{{eta}} restante", + "methods": "Métodos", + "toasts": { + "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", + "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", + "failed_to_delete_all_movies": "Error al eliminar todas las películas", + "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", + "failed_to_delete_all_tvseries": "Error al eliminar todas las series", + "download_cancelled": "Descarga cancelada", + "could_not_cancel_download": "No se pudo cancelar la descarga", + "download_completed": "Descarga completada", + "download_started_for": "Descarga iniciada para {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", + "download_stated_for_item": "Descarga iniciada para {{item}}", + "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", + "download_completed_for_item": "Descarga completada para {{item}}", + "queued_item_for_optimization": "{{item}} en cola para optimización", + "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", + "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", + "no_response_received_from_server": "No se ha recibido respuesta del servidor", + "error_setting_up_the_request": "Error al configurar la petición", + "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", + "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", + "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", + "go_to_downloads": "Ir a descargas" + } + } + }, + "search": { + "search_here": "Buscar aquí...", + "search": "Buscar...", + "x_items": "{{count}} ítems", + "library": "Biblioteca", + "discover": "Descubrir", + "no_results": "Sin resultados", + "no_results_found_for": "No se han encontrado resultados para", + "movies": "Películas", + "series": "Series", + "episodes": "Episodios", + "collections": "Colecciones", + "actors": "Actores", + "request_movies": "Solicitar películas", + "request_series": "Solicitar series", + "recently_added": "Recientemente añadido", + "recent_requests": "Solicitudes recientes", + "plex_watchlist": "Lista de seguimiento de Plex", + "trending": "Trending", + "popular_movies": "Películas populares", + "movie_genres": "Géneros de películas", + "upcoming_movies": "Próximas películas", + "studios": "Estudios", + "popular_tv": "Series populares", + "tv_genres": "Géneros de series", + "upcoming_tv": "Próximas series", + "networks": "Cadenas", + "tmdb_movie_keyword": "Palabra clave de película de TMDB", + "tmdb_movie_genre": "Género de película de TMDB", + "tmdb_tv_keyword": "Palabra clave de serie de TMDB", + "tmdb_tv_genre": "Género de serie de TMDB", + "tmdb_search": "Búsqueda de TMDB", + "tmdb_studio": "Estudio de TMDB", + "tmdb_network": "Cadena de TMDB", + "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", + "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" + }, + "library": { + "no_items_found": "No se han encontrado ítems", + "no_results": "Sin resultados", + "no_libraries_found": "No se han encontrado bibliotecas", + "item_types": { + "movies": "películas", + "series": "series", + "boxsets": "colecciones", + "items": "ítems" + }, + "options": { + "display": "Mostrar", + "row": "Fila", + "list": "Lista", + "image_style": "Estilo de imagen", + "poster": "Poster", + "cover": "Portada", + "show_titles": "Mostrar títulos", + "show_stats": "Mostrar estadísticas" + }, + "filters": { + "genres": "Géneros", + "years": "Años", + "sort_by": "Ordenar por", + "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiquetas" + } + }, + "favorites": { + "series": "Series", + "movies": "Películas", + "episodes": "Episodios", + "videos": "Vídeos", + "boxsets": "Colecciones", + "playlists": "Playlists", + "noDataTitle": "Aún no hay favoritos", + "noData": "Marca elementos como favoritos para verlos aparecer aquí para un acceso rápido." + }, + "custom_links": { + "no_links": "Sin enlaces" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Error al obtener la URL del stream", + "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", + "client_error": "Error del cliente", + "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", + "message_from_server": "Mensaje del servidor: {{message}}", + "video_has_finished_playing": "El vídeo ha terminado de reproducirse", + "no_video_source": "No hay fuente de vídeo...", + "next_episode": "Siguiente episodio", + "refresh_tracks": "Refrescar pistas", + "subtitle_tracks": "Pistas de subtítulos:", + "audio_tracks": "Pistas de audio:", + "playback_state": "Estado de la reproducción:", + "no_data_available": "No hay datos disponibles", + "index": "Índice:" + }, + "item_card": { + "next_up": "A continuación", + "no_items_to_display": "No hay ítems para mostrar", + "cast_and_crew": "Reparto y equipo", + "series": "Series", + "seasons": "Temporadas", + "season": "Temporada", + "no_episodes_for_this_season": "No hay episodios para esta temporada", + "overview": "Resumen", + "more_with": "Más con {{name}}", + "similar_items": "Ítems similares", + "no_similar_items_found": "No se han encontrado ítems similares", + "video": "Vídeo", + "more_details": "Más detalles", + "quality": "Calidad", + "audio": "Audio", + "subtitles": "Subtítulos", + "show_more": "Mostrar más", + "show_less": "Mostrar menos", + "appeared_in": "Apareció en", + "could_not_load_item": "No se pudo cargar el ítem", + "none": "Ninguno", + "download": { + "download_season": "Descargar temporada", + "download_series": "Descargar serie", + "download_episode": "Descargar episodio", + "download_movie": "Descargar película", + "download_x_item": "Descargar {{item_count}} ítems", + "download_button": "Descargar", + "using_optimized_server": "Usando servidor optimizado", + "using_default_method": "Usando método por defecto" + } + }, + "live_tv": { + "next": "Siguiente", + "previous": "Anterior", + "live_tv": "TV en directo", + "coming_soon": "Próximamente", + "on_now": "En directo", + "shows": "Programas", + "movies": "Películas", + "sports": "Deportes", + "for_kids": "Para niños", + "news": "Noticias" + }, + "jellyseerr": { + "confirm": "Confirmar", + "cancel": "Cancelar", + "yes": "Sí", + "whats_wrong": "¿Qué pasa?", + "issue_type": "Tipo de problema", + "select_an_issue": "Selecciona un problema", + "types": "Tipos", + "describe_the_issue": "(opcional) Describe el problema...", + "submit_button": "Enviar", + "report_issue_button": "Reportar problema", + "request_button": "Solicitar", + "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", + "failed_to_login": "Error al iniciar sesión", + "cast": "Reparto", + "details": "Detalles", + "status": "Estado", + "original_title": "Título original", + "series_type": "Tipo de serie", + "release_dates": "Fechas de estreno", + "first_air_date": "Primera fecha de emisión", + "next_air_date": "Próxima fecha de emisión", + "revenue": "Ingresos", + "budget": "Presupuesto", + "original_language": "Idioma original", + "production_country": "País de producción", + "studios": "Estudios", + "network": "Cadena", + "currently_streaming_on": "Actualmente en streaming en", + "advanced": "Avanzado", + "request_as": "Solicitar como", + "tags": "Etiquetas", + "quality_profile": "Perfil de calidad", + "root_folder": "Carpeta raíz", + "season_all": "Season (all)", + "season_number": "Temporada {{season_number}}", + "number_episodes": "{{episode_number}} episodios", + "born": "Nacido", + "appearances": "Apariciones", + "toasts": { + "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", + "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", + "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", + "issue_submitted": "¡Problema enviado!", + "requested_item": "¡{{item}} solicitado!", + "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", + "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" + } + }, + "tabs": { + "home": "Inicio", + "search": "Buscar", + "library": "Bibliotecas", + "custom_links": "Enlaces personalizados", + "favorites": "Favoritos" + } } diff --git a/translations/fr.json b/translations/fr.json index 12f17ef0..1b707c94 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Nom d'utilisateur requis", - "error_title": "Erreur", - "login_title": "Se connecter", - "login_to_title": "Se connecter à", - "username_placeholder": "Nom d'utilisateur", - "password_placeholder": "Mot de passe", - "login_button": "Se connecter", - "quick_connect": "Connexion Rapide", - "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", - "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", - "got_it": "D'accord", - "connection_failed": "La connexion a échoué", - "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", - "an_unexpected_error_occured": "Une erreur inattendue s'est produite", - "change_server": "Changer de serveur", - "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", - "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", - "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", - "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", - "there_is_a_server_error": "Il y a une erreur de serveur", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", - "server_url_placeholder": "http(s)://votre-serveur.com", - "connect_button": "Connexion", - "previous_servers": "Serveurs précédents", - "clear_button": "Effacer", - "search_for_local_servers": "Rechercher des serveurs locaux", - "searching": "Recherche...", - "servers": "Serveurs" - }, - "home": { - "no_internet": "Pas d'Internet", - "no_items": "Aucun média", - "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", - "go_to_downloads": "Aller aux téléchargements", - "oops": "Oups!", - "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", - "continue_watching": "Continuer à regarder", - "next_up": "À suivre", - "recently_added_in": "Ajoutés récemment dans {{libraryName}}", - "suggested_movies": "Films suggérés", - "suggested_episodes": "Épisodes suggérés", - "intro": { - "welcome_to_streamyfin": "Bienvenue sur Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", - "features_title": "Fonctionnalités", - "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", - "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", - "downloads_feature_title": "Téléchargements", - "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", - "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", - "centralised_settings_plugin_title": "Plugin de paramètres centralisés", - "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", - "done_button": "Terminé", - "go_to_settings_button": "Allez dans les paramètres", - "read_more": "Lisez-en plus" - }, - "settings": { - "settings_title": "Paramètres", - "log_out_button": "Déconnexion", - "user_info": { - "user_info_title": "Informations utilisateur", - "user": "Utilisateur", - "server": "Serveur", - "token": "Jeton", - "app_version": "Version de l'application" - }, - "quick_connect": { - "quick_connect_title": "Connexion Rapide", - "authorize_button": "Autoriser Connexion Rapide", - "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", - "success": "Succès", - "quick_connect_autorized": "Connexion Rapide autorisé", - "error": "Erreur", - "invalid_code": "Code invalide", - "authorize": "Autoriser" - }, - "media_controls": { - "media_controls_title": "Contrôles Média", - "forward_skip_length": "Durée de saut en avant", - "rewind_length": "Durée de retour en arrière", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Piste audio de l'élément précédent", - "audio_language": "Langue audio", - "audio_hint": "Choisissez une langue audio par défaut.", - "none": "Aucune", - "language": "Langage" - }, - "subtitles": { - "subtitle_title": "Sous-titres", - "subtitle_language": "Langue des sous-titres", - "subtitle_mode": "Mode des sous-titres", - "set_subtitle_track": "Piste de sous-titres de l'élément précédent", - "subtitle_size": "Taille des sous-titres", - "subtitle_hint": "Configurez les préférences des sous-titres.", - "none": "Aucune", - "language": "Langage", - "loading": "Chargement", - "modes": { - "Default": "Par défaut", - "Smart": "Intelligent", - "Always": "Toujours", - "None": "Aucun", - "OnlyForced": "Forcés seulement" - } - }, - "other": { - "other_title": "Autres", - "follow_device_orientation": "Rotation automatique", - "video_orientation": "Orientation vidéo", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Par défaut", - "ALL": "Toutes", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Haut", - "PORTRAIT_DOWN": "Portrait Bas", - "LANDSCAPE": "Paysage", - "LANDSCAPE_LEFT": "Paysage Gauche", - "LANDSCAPE_RIGHT": "Paysage Droite", - "OTHER": "Autre", - "UNKNOWN": "Inconnu" - }, - "safe_area_in_controls": "Zone de sécurité dans les contrôles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Afficher les liens personnalisés", - "hide_libraries": "Cacher des bibliothèques", - "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", - "disable_haptic_feedback": "Désactiver le retour haptique", - "default_quality": "Qualité par défaut" - }, - "downloads": { - "downloads_title": "Téléchargements", - "download_method": "Méthode de téléchargement", - "remux_max_download": "Téléchargement max remux", - "auto_download": "Téléchargement automatique", - "optimized_versions_server": "Serveur de versions optimisées", - "save_button": "Enregistrer", - "optimized_server": "Serveur optimisé", - "optimized": "Optimisé", - "default": "Par défaut", - "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", - "server_url": "URL du serveur", - "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Mot de passe", - "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", - "save_button": "Enregistrer", - "clear_button": "Effacer", - "login_button": "Connexion", - "total_media_requests": "Total de demandes de médias", - "movie_quota_limit": "Limite de quota de film", - "movie_quota_days": "Jours de quota de film", - "tv_quota_limit": "Limite de quota TV", - "tv_quota_days": "Jours de quota TV", - "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", - "unlimited": "Illimité", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Activer Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port", - "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_marlin": "Lisez-en plus sur Marlin.", - "save_button": "Enregistrer", - "toasts": { - "saved": "Enregistré" - } - } - }, - "storage": { - "storage_title": "Stockage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Appareil {{availableSpace}}%", - "size_used": "{{used}} de {{total}} utilisés", - "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" - }, - "intro": { - "show_intro": "Afficher l'intro", - "reset_intro": "Réinitialiser l'intro" - }, - "logs": { - "logs_title": "Journaux", - "no_logs_available": "Aucun journal disponible", - "delete_all_logs": "Supprimer tous les journaux" - }, - "languages": { - "title": "Langues", - "app_language": "Langue de l'application", - "app_language_description": "Sélectionnez la langue de l'application", - "system": "Système" - }, - "toasts": { - "error_deleting_files": "Erreur lors de la suppression des fichiers", - "background_downloads_enabled": "Téléchargements en arrière-plan activés", - "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", - "connected": "Connecté", - "could_not_connect": "Impossible de se connecter", - "invalid_url": "URL invalide" - } - }, - "downloads": { - "downloads_title": "Téléchargements", - "tvseries": "Séries TV", - "movies": "Films", - "queue": "File d'attente", - "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", - "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", - "no_downloaded_items": "Aucun média téléchargé", - "delete_all_movies_button": "Supprimer tous les films", - "delete_all_tvseries_button": "Supprimer toutes les séries", - "delete_all_button": "Supprimer tout les médias", - "active_download": "Téléchargement actif", - "no_active_downloads": "Aucun téléchargements actifs", - "active_downloads": "Téléchargements actifs", - "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", - "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", - "back": "Retour", - "delete": "Supprimer", - "something_went_wrong": "Quelque chose s'est mal passé", - "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Méthodes", - "toasts": { - "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", - "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", - "failed_to_delete_all_movies": "Échec de la suppression de tous les films", - "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", - "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", - "download_cancelled": "Téléchargement annulé", - "could_not_cancel_download": "Impossible d'annuler le téléchargement", - "download_completed": "Téléchargement terminé", - "download_started_for": "Téléchargement démarré pour {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", - "download_stated_for_item": "Téléchargement démarré pour {{item}}", - "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", - "download_completed_for_item": "Téléchargement terminé pour {{item}}", - "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", - "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", - "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", - "no_response_received_from_server": "Aucune réponse reçue du serveur", - "error_setting_up_the_request": "Erreur lors de la configuration de la demande", - "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", - "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", - "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", - "go_to_downloads": "Aller aux téléchargements" - } - } - }, - "search": { - "search_here": "Rechercher ici...", - "search": "Rechercher...", - "x_items": "{{count}} médias", - "library": "Bibliothèque", - "discover": "Découvrir", - "no_results": "Aucun résultat", - "no_results_found_for": "Aucun résultat trouvé pour", - "movies": "Films", - "series": "Séries", - "episodes": "Épisodes", - "collections": "Collections", - "actors": "Acteurs", - "request_movies": "Demander un film", - "request_series": "Demander une série", - "recently_added": "Ajoutés récemment", - "recent_requests": "Demandes récentes", - "plex_watchlist": "Liste de lecture Plex", - "trending": "Tendance", - "popular_movies": "Films populaires", - "movie_genres": "Genres de films", - "upcoming_movies": "Films à venir", - "studios": "Studios", - "popular_tv": "TV populaire", - "tv_genres": "Genres TV", - "upcoming_tv": "TV à venir", - "networks": "Réseaux", - "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", - "tmdb_movie_genre": "Genre de film TMDB", - "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", - "tmdb_tv_genre": "Genre TV TMDB", - "tmdb_search": "Recherche TMDB", - "tmdb_studio": "Studio TMDB", - "tmdb_network": "Réseau TMDB", - "tmdb_movie_streaming_services": "Services de streaming de films TMDB", - "tmdb_tv_streaming_services": "Services de streaming TV TMDB" - }, - "library": { - "no_items_found": "Aucun média trouvé", - "no_results": "Aucun résultat", - "no_libraries_found": "Aucune bibliothèque trouvée", - "item_types": { - "movies": "films", - "series": "séries", - "boxsets": "coffrets", - "items": "médias" - }, - "options": { - "display": "Affichage", - "row": "Rangée", - "list": "Liste", - "image_style": "Style d'image", - "poster": "Affiche", - "cover": "Couverture", - "show_titles": "Afficher les titres", - "show_stats": "Afficher les statistiques" - }, - "filters": { - "genres": "Genres", - "years": "Années", - "sort_by": "Trier par", - "sort_order": "Ordre de tri", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Séries", - "movies": "Films", - "episodes": "Épisodes", - "videos": "Vidéos", - "boxsets": "Coffrets", - "playlists": "Listes de lecture", - "noDataTitle": "Pas encore de favoris", - "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." - }, - "custom_links": { - "no_links": "Aucuns liens" - }, - "player": { - "error": "Erreur", - "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", - "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", - "client_error": "Erreur client", - "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", - "message_from_server": "Message du serveur: {{message}}", - "video_has_finished_playing": "La vidéo a fini de jouer!", - "no_video_source": "Aucune source vidéo...", - "next_episode": "Épisode suivant", - "refresh_tracks": "Rafraîchir les pistes", - "subtitle_tracks": "Pistes de sous-titres:", - "audio_tracks": "Pistes audio:", - "playback_state": "État de lecture:", - "no_data_available": "Aucune donnée disponible", - "index": "Index:" - }, - "item_card": { - "next_up": "À suivre", - "no_items_to_display": "Aucun médias à afficher", - "cast_and_crew": "Distribution et équipe", - "series": "Séries", - "seasons": "Saisons", - "season": "Saison", - "no_episodes_for_this_season": "Aucun épisode pour cette saison", - "overview": "Aperçu", - "more_with": "Plus avec {{name}}", - "similar_items": "Médias similaires", - "no_similar_items_found": "Aucun média similaire trouvé", - "video": "Vidéo", - "more_details": "Plus de détails", - "quality": "Qualité", - "audio": "Audio", - "subtitles": "Sous-titres", - "show_more": "Afficher plus", - "show_less": "Afficher moins", - "appeared_in": "Apparu dans", - "could_not_load_item": "Impossible de charger le média", - "none": "Aucun", - "download": { - "download_season": "Télécharger la saison", - "download_series": "Télécharger la série", - "download_episode": "Télécharger l'épisode", - "download_movie": "Télécharger le film", - "download_x_item": "Télécharger {{item_count}} médias", - "download_button": "Télécharger", - "using_optimized_server": "Avec le serveur optimisées", - "using_default_method": "Avec la méthode par défaut" - } - }, - "live_tv": { - "next": "Suivant", - "previous": "Précédent", - "live_tv": "TV en direct", - "coming_soon": "Bientôt", - "on_now": "En ce moment", - "shows": "Émissions", - "movies": "Films", - "sports": "Sports", - "for_kids": "Pour enfants", - "news": "Actualités" - }, - "jellyseerr": { - "confirm": "Confirmer", - "cancel": "Annuler", - "yes": "Oui", - "whats_wrong": "Quel est le problème?", - "issue_type": "Type de problème", - "select_an_issue": "Sélectionnez un problème", - "types": "Types", - "describe_the_issue": "(optionnel) Décrivez le problème...", - "submit_button": "Soumettre", - "report_issue_button": "Signaler un problème", - "request_button": "Demander", - "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", - "failed_to_login": "Échec de la connexion", - "cast": "Distribution", - "details": "Détails", - "status": "Statut", - "original_title": "Titre original", - "series_type": "Type de série", - "release_dates": "Dates de sortie", - "first_air_date": "Date de première diffusion", - "next_air_date": "Date de prochaine diffusion", - "revenue": "Revenu", - "budget": "Budget", - "original_language": "Langue originale", - "production_country": "Pays de production", - "studios": "Studios", - "network": "Réseaux", - "currently_streaming_on": "En streaming sur", - "advanced": "Avancé", - "request_as": "Demander en tant que", - "tags": "Tags", - "quality_profile": "Profil de qualité", - "root_folder": "Dossier racine", - "season_all": "Season (all)", - "season_number": "Saison {{season_number}}", - "number_episodes": "{{episode_number}} épisodes", - "born": "Né(e) le", - "appearances": "Apparences", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", - "jellyseerr_test_failed": "Échec du test de Jellyseerr", - "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", - "issue_submitted": "Problème soumis!", - "requested_item": "{{item}}} demandé!", - "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", - "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" - } - }, - "tabs": { - "home": "Accueil", - "search": "Recherche", - "library": "Bibliothèque", - "custom_links": "Liens personnalisés", - "favorites": "Favoris" - } + "login": { + "username_required": "Nom d'utilisateur requis", + "error_title": "Erreur", + "login_title": "Se connecter", + "login_to_title": "Se connecter à", + "username_placeholder": "Nom d'utilisateur", + "password_placeholder": "Mot de passe", + "login_button": "Se connecter", + "quick_connect": "Connexion Rapide", + "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", + "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", + "got_it": "D'accord", + "connection_failed": "La connexion a échoué", + "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", + "an_unexpected_error_occured": "Une erreur inattendue s'est produite", + "change_server": "Changer de serveur", + "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", + "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", + "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", + "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", + "there_is_a_server_error": "Il y a une erreur de serveur", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", + "server_url_placeholder": "http(s)://votre-serveur.com", + "connect_button": "Connexion", + "previous_servers": "Serveurs précédents", + "clear_button": "Effacer", + "search_for_local_servers": "Rechercher des serveurs locaux", + "searching": "Recherche...", + "servers": "Serveurs" + }, + "home": { + "no_internet": "Pas d'Internet", + "no_items": "Aucun média", + "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", + "go_to_downloads": "Aller aux téléchargements", + "oops": "Oups!", + "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", + "continue_watching": "Continuer à regarder", + "next_up": "À suivre", + "recently_added_in": "Ajoutés récemment dans {{libraryName}}", + "suggested_movies": "Films suggérés", + "suggested_episodes": "Épisodes suggérés", + "intro": { + "welcome_to_streamyfin": "Bienvenue sur Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", + "features_title": "Fonctionnalités", + "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", + "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", + "downloads_feature_title": "Téléchargements", + "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", + "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", + "centralised_settings_plugin_title": "Plugin de paramètres centralisés", + "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", + "done_button": "Terminé", + "go_to_settings_button": "Allez dans les paramètres", + "read_more": "Lisez-en plus" + }, + "settings": { + "settings_title": "Paramètres", + "log_out_button": "Déconnexion", + "user_info": { + "user_info_title": "Informations utilisateur", + "user": "Utilisateur", + "server": "Serveur", + "token": "Jeton", + "app_version": "Version de l'application" + }, + "quick_connect": { + "quick_connect_title": "Connexion Rapide", + "authorize_button": "Autoriser Connexion Rapide", + "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", + "success": "Succès", + "quick_connect_autorized": "Connexion Rapide autorisé", + "error": "Erreur", + "invalid_code": "Code invalide", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Contrôles Média", + "forward_skip_length": "Durée de saut en avant", + "rewind_length": "Durée de retour en arrière", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Piste audio de l'élément précédent", + "audio_language": "Langue audio", + "audio_hint": "Choisissez une langue audio par défaut.", + "none": "Aucune", + "language": "Langage" + }, + "subtitles": { + "subtitle_title": "Sous-titres", + "subtitle_language": "Langue des sous-titres", + "subtitle_mode": "Mode des sous-titres", + "set_subtitle_track": "Piste de sous-titres de l'élément précédent", + "subtitle_size": "Taille des sous-titres", + "subtitle_hint": "Configurez les préférences des sous-titres.", + "none": "Aucune", + "language": "Langage", + "loading": "Chargement", + "modes": { + "Default": "Par défaut", + "Smart": "Intelligent", + "Always": "Toujours", + "None": "Aucun", + "OnlyForced": "Forcés seulement" + } + }, + "other": { + "other_title": "Autres", + "follow_device_orientation": "Rotation automatique", + "video_orientation": "Orientation vidéo", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Par défaut", + "ALL": "Toutes", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Haut", + "PORTRAIT_DOWN": "Portrait Bas", + "LANDSCAPE": "Paysage", + "LANDSCAPE_LEFT": "Paysage Gauche", + "LANDSCAPE_RIGHT": "Paysage Droite", + "OTHER": "Autre", + "UNKNOWN": "Inconnu" + }, + "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Afficher les liens personnalisés", + "hide_libraries": "Cacher des bibliothèques", + "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", + "disable_haptic_feedback": "Désactiver le retour haptique", + "default_quality": "Qualité par défaut" + }, + "downloads": { + "downloads_title": "Téléchargements", + "download_method": "Méthode de téléchargement", + "remux_max_download": "Téléchargement max remux", + "auto_download": "Téléchargement automatique", + "optimized_versions_server": "Serveur de versions optimisées", + "save_button": "Enregistrer", + "optimized_server": "Serveur optimisé", + "optimized": "Optimisé", + "default": "Par défaut", + "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", + "server_url": "URL du serveur", + "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Mot de passe", + "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", + "save_button": "Enregistrer", + "clear_button": "Effacer", + "login_button": "Connexion", + "total_media_requests": "Total de demandes de médias", + "movie_quota_limit": "Limite de quota de film", + "movie_quota_days": "Jours de quota de film", + "tv_quota_limit": "Limite de quota TV", + "tv_quota_days": "Jours de quota TV", + "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", + "unlimited": "Illimité", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Activer Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port", + "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_marlin": "Lisez-en plus sur Marlin.", + "save_button": "Enregistrer", + "toasts": { + "saved": "Enregistré" + } + } + }, + "storage": { + "storage_title": "Stockage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Appareil {{availableSpace}}%", + "size_used": "{{used}} de {{total}} utilisés", + "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" + }, + "intro": { + "show_intro": "Afficher l'intro", + "reset_intro": "Réinitialiser l'intro" + }, + "logs": { + "logs_title": "Journaux", + "no_logs_available": "Aucun journal disponible", + "delete_all_logs": "Supprimer tous les journaux" + }, + "languages": { + "title": "Langues", + "app_language": "Langue de l'application", + "app_language_description": "Sélectionnez la langue de l'application", + "system": "Système" + }, + "toasts": { + "error_deleting_files": "Erreur lors de la suppression des fichiers", + "background_downloads_enabled": "Téléchargements en arrière-plan activés", + "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", + "connected": "Connecté", + "could_not_connect": "Impossible de se connecter", + "invalid_url": "URL invalide" + } + }, + "downloads": { + "downloads_title": "Téléchargements", + "tvseries": "Séries TV", + "movies": "Films", + "queue": "File d'attente", + "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", + "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", + "no_downloaded_items": "Aucun média téléchargé", + "delete_all_movies_button": "Supprimer tous les films", + "delete_all_tvseries_button": "Supprimer toutes les séries", + "delete_all_button": "Supprimer tout les médias", + "active_download": "Téléchargement actif", + "no_active_downloads": "Aucun téléchargements actifs", + "active_downloads": "Téléchargements actifs", + "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", + "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", + "back": "Retour", + "delete": "Supprimer", + "something_went_wrong": "Quelque chose s'est mal passé", + "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Méthodes", + "toasts": { + "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", + "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", + "failed_to_delete_all_movies": "Échec de la suppression de tous les films", + "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", + "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", + "download_cancelled": "Téléchargement annulé", + "could_not_cancel_download": "Impossible d'annuler le téléchargement", + "download_completed": "Téléchargement terminé", + "download_started_for": "Téléchargement démarré pour {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", + "download_stated_for_item": "Téléchargement démarré pour {{item}}", + "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", + "download_completed_for_item": "Téléchargement terminé pour {{item}}", + "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", + "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", + "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", + "no_response_received_from_server": "Aucune réponse reçue du serveur", + "error_setting_up_the_request": "Erreur lors de la configuration de la demande", + "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", + "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", + "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", + "go_to_downloads": "Aller aux téléchargements" + } + } + }, + "search": { + "search_here": "Rechercher ici...", + "search": "Rechercher...", + "x_items": "{{count}} médias", + "library": "Bibliothèque", + "discover": "Découvrir", + "no_results": "Aucun résultat", + "no_results_found_for": "Aucun résultat trouvé pour", + "movies": "Films", + "series": "Séries", + "episodes": "Épisodes", + "collections": "Collections", + "actors": "Acteurs", + "request_movies": "Demander un film", + "request_series": "Demander une série", + "recently_added": "Ajoutés récemment", + "recent_requests": "Demandes récentes", + "plex_watchlist": "Liste de lecture Plex", + "trending": "Tendance", + "popular_movies": "Films populaires", + "movie_genres": "Genres de films", + "upcoming_movies": "Films à venir", + "studios": "Studios", + "popular_tv": "TV populaire", + "tv_genres": "Genres TV", + "upcoming_tv": "TV à venir", + "networks": "Réseaux", + "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", + "tmdb_movie_genre": "Genre de film TMDB", + "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", + "tmdb_tv_genre": "Genre TV TMDB", + "tmdb_search": "Recherche TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Réseau TMDB", + "tmdb_movie_streaming_services": "Services de streaming de films TMDB", + "tmdb_tv_streaming_services": "Services de streaming TV TMDB" + }, + "library": { + "no_items_found": "Aucun média trouvé", + "no_results": "Aucun résultat", + "no_libraries_found": "Aucune bibliothèque trouvée", + "item_types": { + "movies": "films", + "series": "séries", + "boxsets": "coffrets", + "items": "médias" + }, + "options": { + "display": "Affichage", + "row": "Rangée", + "list": "Liste", + "image_style": "Style d'image", + "poster": "Affiche", + "cover": "Couverture", + "show_titles": "Afficher les titres", + "show_stats": "Afficher les statistiques" + }, + "filters": { + "genres": "Genres", + "years": "Années", + "sort_by": "Trier par", + "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Séries", + "movies": "Films", + "episodes": "Épisodes", + "videos": "Vidéos", + "boxsets": "Coffrets", + "playlists": "Listes de lecture", + "noDataTitle": "Pas encore de favoris", + "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." + }, + "custom_links": { + "no_links": "Aucuns liens" + }, + "player": { + "error": "Erreur", + "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", + "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", + "client_error": "Erreur client", + "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", + "message_from_server": "Message du serveur: {{message}}", + "video_has_finished_playing": "La vidéo a fini de jouer!", + "no_video_source": "Aucune source vidéo...", + "next_episode": "Épisode suivant", + "refresh_tracks": "Rafraîchir les pistes", + "subtitle_tracks": "Pistes de sous-titres:", + "audio_tracks": "Pistes audio:", + "playback_state": "État de lecture:", + "no_data_available": "Aucune donnée disponible", + "index": "Index:" + }, + "item_card": { + "next_up": "À suivre", + "no_items_to_display": "Aucun médias à afficher", + "cast_and_crew": "Distribution et équipe", + "series": "Séries", + "seasons": "Saisons", + "season": "Saison", + "no_episodes_for_this_season": "Aucun épisode pour cette saison", + "overview": "Aperçu", + "more_with": "Plus avec {{name}}", + "similar_items": "Médias similaires", + "no_similar_items_found": "Aucun média similaire trouvé", + "video": "Vidéo", + "more_details": "Plus de détails", + "quality": "Qualité", + "audio": "Audio", + "subtitles": "Sous-titres", + "show_more": "Afficher plus", + "show_less": "Afficher moins", + "appeared_in": "Apparu dans", + "could_not_load_item": "Impossible de charger le média", + "none": "Aucun", + "download": { + "download_season": "Télécharger la saison", + "download_series": "Télécharger la série", + "download_episode": "Télécharger l'épisode", + "download_movie": "Télécharger le film", + "download_x_item": "Télécharger {{item_count}} médias", + "download_button": "Télécharger", + "using_optimized_server": "Avec le serveur optimisées", + "using_default_method": "Avec la méthode par défaut" + } + }, + "live_tv": { + "next": "Suivant", + "previous": "Précédent", + "live_tv": "TV en direct", + "coming_soon": "Bientôt", + "on_now": "En ce moment", + "shows": "Émissions", + "movies": "Films", + "sports": "Sports", + "for_kids": "Pour enfants", + "news": "Actualités" + }, + "jellyseerr": { + "confirm": "Confirmer", + "cancel": "Annuler", + "yes": "Oui", + "whats_wrong": "Quel est le problème?", + "issue_type": "Type de problème", + "select_an_issue": "Sélectionnez un problème", + "types": "Types", + "describe_the_issue": "(optionnel) Décrivez le problème...", + "submit_button": "Soumettre", + "report_issue_button": "Signaler un problème", + "request_button": "Demander", + "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", + "failed_to_login": "Échec de la connexion", + "cast": "Distribution", + "details": "Détails", + "status": "Statut", + "original_title": "Titre original", + "series_type": "Type de série", + "release_dates": "Dates de sortie", + "first_air_date": "Date de première diffusion", + "next_air_date": "Date de prochaine diffusion", + "revenue": "Revenu", + "budget": "Budget", + "original_language": "Langue originale", + "production_country": "Pays de production", + "studios": "Studios", + "network": "Réseaux", + "currently_streaming_on": "En streaming sur", + "advanced": "Avancé", + "request_as": "Demander en tant que", + "tags": "Tags", + "quality_profile": "Profil de qualité", + "root_folder": "Dossier racine", + "season_all": "Season (all)", + "season_number": "Saison {{season_number}}", + "number_episodes": "{{episode_number}} épisodes", + "born": "Né(e) le", + "appearances": "Apparences", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", + "jellyseerr_test_failed": "Échec du test de Jellyseerr", + "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", + "issue_submitted": "Problème soumis!", + "requested_item": "{{item}}} demandé!", + "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", + "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" + } + }, + "tabs": { + "home": "Accueil", + "search": "Recherche", + "library": "Bibliothèque", + "custom_links": "Liens personnalisés", + "favorites": "Favoris" + } } diff --git a/translations/it.json b/translations/it.json index 38e96cc6..44f437b3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Nome utente è obbligatorio", - "error_title": "Errore", - "login_title": "Accesso", - "login_to_title": "Accedi a", - "username_placeholder": "Nome utente", - "password_placeholder": "Password", - "login_button": "Accedi", - "quick_connect": "Connessione Rapida", - "enter_code_to_login": "Inserire {{code}} per accedere", - "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", - "got_it": "Capito", - "connection_failed": "Connessione fallita", - "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", - "an_unexpected_error_occured": "Si è verificato un errore inaspettato", - "change_server": "Cambiare il server", - "invalid_username_or_password": "Nome utente o password non validi", - "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", - "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", - "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", - "there_is_a_server_error": "Si è verificato un errore del server", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" - }, - "server": { - "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", - "server_url_placeholder": "http(s)://tuo-server.com", - "connect_button": "Connetti", - "previous_servers": "server precedente", - "clear_button": "Cancella", - "search_for_local_servers": "Ricerca dei server locali", - "searching": "Cercando...", - "servers": "Servers" - }, - "home": { - "no_internet": "Nessun Internet", - "no_items": "Nessun oggetto", - "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", - "go_to_downloads": "Vai agli elementi scaricati", - "oops": "Oops!", - "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", - "continue_watching": "Continua a guardare", - "next_up": "Prossimo", - "recently_added_in": "Aggiunti di recente a {{libraryName}}", - "suggested_movies": "Film consigliati", - "suggested_episodes": "Episodi consigliati", - "intro": { - "welcome_to_streamyfin": "Benvenuto a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", - "features_title": "Funzioni", - "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", - "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", - "downloads_feature_title": "Scaricamento", - "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", - "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", - "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", - "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", - "done_button": "Fatto", - "go_to_settings_button": "Vai alle impostazioni", - "read_more": "Leggi di più" - }, - "settings": { - "settings_title": "Impostazioni", - "log_out_button": "Esci", - "user_info": { - "user_info_title": "Info utente", - "user": "Utente", - "server": "Server", - "token": "Token", - "app_version": "Versione dell'App" - }, - "quick_connect": { - "quick_connect_title": "Connessione Rapida", - "authorize_button": "Autorizza Connessione Rapida", - "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", - "success": "Successo", - "quick_connect_autorized": "Connessione Rapida autorizzata", - "error": "Errore", - "invalid_code": "Codice invalido", - "authorize": "Autorizza" - }, - "media_controls": { - "media_controls_title": "Controlli multimediali", - "forward_skip_length": "Lunghezza del salto in avanti", - "rewind_length": "Lunghezza del riavvolgimento", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Imposta la traccia audio dall'elemento precedente", - "audio_language": "Lingua Audio", - "audio_hint": "Scegli la lingua audio predefinita.", - "none": "Nessuno", - "language": "Lingua" - }, - "subtitles": { - "subtitle_title": "Sottotitoli", - "subtitle_language": "Lingua dei sottotitoli", - "subtitle_mode": "Modalità dei sottotitoli", - "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", - "subtitle_size": "Dimensione dei sottotitoli", - "subtitle_hint": "Configura la preferenza dei sottotitoli.", - "none": "Nessuno", - "language": "Lingua", - "loading": "Caricamento", - "modes": { - "Default": "Predefinito", - "Smart": "Intelligente", - "Always": "Sempre", - "None": "Nessuno", - "OnlyForced": "Solo forzati" - } - }, - "other": { - "other_title": "Altro", - "follow_device_orientation": "Rotazione automatica", - "video_orientation": "Orientamento del video", - "orientation": "Orientamento", - "orientations": { - "DEFAULT": "Predefinito", - "ALL": "Tutto", - "PORTRAIT": "Verticale", - "PORTRAIT_UP": "Verticale sopra", - "PORTRAIT_DOWN": "Verticale sotto", - "LANDSCAPE": "Orizzontale", - "LANDSCAPE_LEFT": "Orizzontale sinitra", - "LANDSCAPE_RIGHT": "Orizzontale destra", - "OTHER": "Altro", - "UNKNOWN": "Sconosciuto" - }, - "safe_area_in_controls": "Area sicura per i controlli", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostra i link del menu personalizzato", - "hide_libraries": "Nascondi Librerie", - "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", - "disable_haptic_feedback": "Disabilita il feedback aptico", - "default_quality": "Qualità predefinita" - }, - "downloads": { - "downloads_title": "Scaricamento", - "download_method": "Metodo per lo scaricamento", - "remux_max_download": "Numero di Remux da scaricare al massimo", - "auto_download": "Scaricamento automatico", - "optimized_versions_server": "Versioni del server di ottimizzazione", - "save_button": "Salva", - "optimized_server": "Server di ottimizzazione", - "optimized": "Ottimizzato", - "default": "Predefinito", - "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta" - }, - "plugins": { - "plugins_title": "Plugin", - "jellyseerr": { - "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", - "server_url": "URL del Server", - "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", - "server_url_placeholder": "URL di Jellyseerr...", - "password": "Password", - "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", - "save_button": "Salva", - "clear_button": "Cancella", - "login_button": "Accedi", - "total_media_requests": "Totale di richieste di media", - "movie_quota_limit": "Limite di quota per i film", - "movie_quota_days": "Giorni di quota per i film", - "tv_quota_limit": "Limite di quota per le serie TV", - "tv_quota_days": "Giorni di quota per le serie TV", - "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", - "unlimited": "Illimitato", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Abilita la ricerca Marlin ", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta", - "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_marlin": "Leggi di più su Marlin.", - "save_button": "Salva", - "toasts": { - "saved": "Salvato" - } - } - }, - "storage": { - "storage_title": "Spazio", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} di {{total}} usato", - "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" - }, - "intro": { - "show_intro": "Mostra intro", - "reset_intro": "Ripristina intro" - }, - "logs": { - "logs_title": "Log", - "no_logs_available": "Nessun log disponibile", - "delete_all_logs": "Cancella tutti i log" - }, - "languages": { - "title": "Lingue", - "app_language": "Lingua dell'App", - "app_language_description": "Selezione la lingua dell'app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Errore nella cancellazione dei file", - "background_downloads_enabled": "Scaricamento in background abilitato", - "background_downloads_disabled": "Scaricamento in background disabilitato", - "connected": "Connesso", - "could_not_connect": "Non è stato possibile connettersi", - "invalid_url": "URL invalido" - } - }, - "downloads": { - "downloads_title": "Scaricati", - "tvseries": "Serie TV", - "movies": "Film", - "queue": "Coda", - "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", - "no_items_in_queue": "Nessun elemento in coda", - "no_downloaded_items": "Nessun elemento scaricato", - "delete_all_movies_button": "Cancella tutti i film", - "delete_all_tvseries_button": "Cancella tutte le serie TV", - "delete_all_button": "Cancella tutti", - "active_download": "Scaricamento in corso", - "no_active_downloads": "Nessun scaricamento in corso", - "active_downloads": "Scaricamenti in corso", - "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", - "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", - "back": "Indietro", - "delete": "Cancella", - "something_went_wrong": "Qualcosa è andato storto", - "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Metodi", - "toasts": { - "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", - "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", - "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", - "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", - "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", - "download_cancelled": "Scaricamento annullato", - "could_not_cancel_download": "Impossibile annullare lo scaricamento", - "download_completed": "Scaricamento completato", - "download_started_for": "Scaricamento iniziato per {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", - "download_stated_for_item": "Scaricamento iniziato per {{item}}", - "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", - "download_completed_for_item": "Scaricamento completato per {{item}}", - "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", - "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", - "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", - "go_to_downloads": "Vai agli elementi scaricati" - } - } - }, - "search": { - "search_here": "Cerca qui...", - "search": "Cerca...", - "x_items": "{{count}} elementi", - "library": "Libreria", - "discover": "Scopri", - "no_results": "Nessun risultato", - "no_results_found_for": "Nessun risultato trovato per", - "movies": "Film", - "series": "Serie", - "episodes": "Episodi", - "collections": "Collezioni", - "actors": "Attori", - "request_movies": "Film Richiesti", - "request_series": "Serie Richieste", - "recently_added": "Aggiunti di Recente", - "recent_requests": "Richiesti di Recente", - "plex_watchlist": "Plex Watchlist", - "trending": "In tendenza", - "popular_movies": "Film Popolari", - "movie_genres": "Generi Film", - "upcoming_movies": "Film in arrivo", - "studios": "Studio", - "popular_tv": "Serie Popolari", - "tv_genres": "Generi Televisivi", - "upcoming_tv": "Serie in Arrivo", - "networks": "Network", - "tmdb_movie_keyword": "TMDB Parola chiave del film", - "tmdb_movie_genre": "TMDB Genere Film", - "tmdb_tv_keyword": "TMDB Parola chiave della serie", - "tmdb_tv_genre": "TMDB Genere Televisivo", - "tmdb_search": "TMDB Cerca", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", - "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" - }, - "library": { - "no_items_found": "Nessun elemento trovato", - "no_results": "Nessun risultato", - "no_libraries_found": "Nessuna libreria trovata", - "item_types": { - "movies": "film", - "series": "serie TV", - "boxsets": "cofanetti", - "items": "elementi" - }, - "options": { - "display": "Display", - "row": "Fila", - "list": "Lista", - "image_style": "Stile dell'immagine", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Mostra titoli", - "show_stats": "Mostra statistiche" - }, - "filters": { - "genres": "Generi", - "years": "Anni", - "sort_by": "Ordina per", - "sort_order": "Criterio di ordinamento", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tag" - } - }, - "favorites": { - "series": "Serie TV", - "movies": "Film", - "episodes": "Episodi", - "videos": "Video", - "boxsets": "Boxset", - "playlists": "Playlist", - "noDataTitle": "Ancora nessun preferito", - "noData": "Contrassegna gli elementi come preferiti per vederli apparire qui per un accesso rapido." - }, - "custom_links": { - "no_links": "Nessun link" - }, - "player": { - "error": "Errore", - "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", - "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", - "client_error": "Errore del client", - "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", - "message_from_server": "Messaggio dal server", - "video_has_finished_playing": "La riproduzione del video è terminata!", - "no_video_source": "Nessuna sorgente video...", - "next_episode": "Prossimo Episodio", - "refresh_tracks": "Aggiorna tracce", - "subtitle_tracks": "Tracce di sottotitoli:", - "audio_tracks": "Tracce audio:", - "playback_state": "Stato della riproduzione:", - "no_data_available": "Nessun dato disponibile", - "index": "Indice:" - }, - "item_card": { - "next_up": "Il prossimo", - "no_items_to_display": "Nessun elemento da visualizzare", - "cast_and_crew": "Cast e Equipaggio", - "series": "Serie", - "seasons": "Stagioni", - "season": "Stagione", - "no_episodes_for_this_season": "Nessun episodio per questa stagione", - "overview": "Panoramica", - "more_with": "Altri con {{name}}", - "similar_items": "Elementi simili", - "no_similar_items_found": "Non sono stati trovati elementi simili", - "video": "Video", - "more_details": "Più dettagli", - "quality": "Qualità", - "audio": "Audio", - "subtitles": "Sottotitoli", - "show_more": "Mostra di più", - "show_less": "Mostra di meno", - "appeared_in": "Apparso in", - "could_not_load_item": "Impossibile caricare l'elemento", - "none": "Nessuno", - "download": { - "download_season": "Scarica Stagione", - "download_series": "Scarica Serie", - "download_episode": "Scarica Episodio", - "download_movie": "Scarica Film", - "download_x_item": "Scarica {{item_count}} elementi", - "download_button": "Scarica", - "using_optimized_server": "Utilizzando il server di ottimizzazione", - "using_default_method": "Utilizzando il metodo predefinito" - } - }, - "live_tv": { - "next": "Prossimo", - "previous": "Precedente", - "live_tv": "TV in diretta", - "coming_soon": "Prossimamente", - "on_now": "In onda ora", - "shows": "Programmi", - "movies": "Film", - "sports": "Sport", - "for_kids": "Per Bambini", - "news": "Notiziari" - }, - "jellyseerr": { - "confirm": "Conferma", - "cancel": "Cancella", - "yes": "Si", - "whats_wrong": "Cosa c'è che non va?", - "issue_type": "Tipo di problema", - "select_an_issue": "Seleziona un problema", - "types": "Tipi", - "describe_the_issue": "(facoltativo) Descrivere il problema...", - "submit_button": "Invia", - "report_issue_button": "Segnalare il problema", - "request_button": "Richiedi", - "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", - "failed_to_login": "Accesso non riuscito", - "cast": "Cast", - "details": "Dettagli", - "status": "Stato", - "original_title": "Titolo originale", - "series_type": "Tipo di Serie", - "release_dates": "Date di Uscita", - "first_air_date": "Prima Data di Messa in Onda", - "next_air_date": "Prossima Data di Messa in Onda", - "revenue": "Ricavi", - "budget": "Budget", - "original_language": "Lingua Originale", - "production_country": "Paese di Produzione", - "studios": "Studio", - "network": "Network", - "currently_streaming_on": "Attualmente in streaming su", - "advanced": "Avanzate", - "request_as": "Richiedi Come", - "tags": "Tag", - "quality_profile": "Profilo qualità", - "root_folder": "Cartella radice", - "season_all": "Season (all)", - "season_number": "Stagione {{season_number}}", - "number_episodes": "{{episode_number}} Episodio", - "born": "Nato", - "appearances": "Aspetto", - "toasts": { - "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", - "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", - "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", - "issue_submitted": "Problema inviato!", - "requested_item": "Richiesto {{item}}!", - "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", - "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" - } - }, - "tabs": { - "home": "Home", - "search": "Cerca", - "library": "Libreria", - "custom_links": "Collegamenti personalizzati", - "favorites": "Preferiti" - } + "login": { + "username_required": "Nome utente è obbligatorio", + "error_title": "Errore", + "login_title": "Accesso", + "login_to_title": "Accedi a", + "username_placeholder": "Nome utente", + "password_placeholder": "Password", + "login_button": "Accedi", + "quick_connect": "Connessione Rapida", + "enter_code_to_login": "Inserire {{code}} per accedere", + "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", + "got_it": "Capito", + "connection_failed": "Connessione fallita", + "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", + "an_unexpected_error_occured": "Si è verificato un errore inaspettato", + "change_server": "Cambiare il server", + "invalid_username_or_password": "Nome utente o password non validi", + "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", + "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", + "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", + "there_is_a_server_error": "Si è verificato un errore del server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" + }, + "server": { + "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", + "server_url_placeholder": "http(s)://tuo-server.com", + "connect_button": "Connetti", + "previous_servers": "server precedente", + "clear_button": "Cancella", + "search_for_local_servers": "Ricerca dei server locali", + "searching": "Cercando...", + "servers": "Servers" + }, + "home": { + "no_internet": "Nessun Internet", + "no_items": "Nessun oggetto", + "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", + "go_to_downloads": "Vai agli elementi scaricati", + "oops": "Oops!", + "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", + "continue_watching": "Continua a guardare", + "next_up": "Prossimo", + "recently_added_in": "Aggiunti di recente a {{libraryName}}", + "suggested_movies": "Film consigliati", + "suggested_episodes": "Episodi consigliati", + "intro": { + "welcome_to_streamyfin": "Benvenuto a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", + "features_title": "Funzioni", + "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", + "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", + "downloads_feature_title": "Scaricamento", + "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", + "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", + "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", + "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", + "done_button": "Fatto", + "go_to_settings_button": "Vai alle impostazioni", + "read_more": "Leggi di più" + }, + "settings": { + "settings_title": "Impostazioni", + "log_out_button": "Esci", + "user_info": { + "user_info_title": "Info utente", + "user": "Utente", + "server": "Server", + "token": "Token", + "app_version": "Versione dell'App" + }, + "quick_connect": { + "quick_connect_title": "Connessione Rapida", + "authorize_button": "Autorizza Connessione Rapida", + "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", + "success": "Successo", + "quick_connect_autorized": "Connessione Rapida autorizzata", + "error": "Errore", + "invalid_code": "Codice invalido", + "authorize": "Autorizza" + }, + "media_controls": { + "media_controls_title": "Controlli multimediali", + "forward_skip_length": "Lunghezza del salto in avanti", + "rewind_length": "Lunghezza del riavvolgimento", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Imposta la traccia audio dall'elemento precedente", + "audio_language": "Lingua Audio", + "audio_hint": "Scegli la lingua audio predefinita.", + "none": "Nessuno", + "language": "Lingua" + }, + "subtitles": { + "subtitle_title": "Sottotitoli", + "subtitle_language": "Lingua dei sottotitoli", + "subtitle_mode": "Modalità dei sottotitoli", + "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", + "subtitle_size": "Dimensione dei sottotitoli", + "subtitle_hint": "Configura la preferenza dei sottotitoli.", + "none": "Nessuno", + "language": "Lingua", + "loading": "Caricamento", + "modes": { + "Default": "Predefinito", + "Smart": "Intelligente", + "Always": "Sempre", + "None": "Nessuno", + "OnlyForced": "Solo forzati" + } + }, + "other": { + "other_title": "Altro", + "follow_device_orientation": "Rotazione automatica", + "video_orientation": "Orientamento del video", + "orientation": "Orientamento", + "orientations": { + "DEFAULT": "Predefinito", + "ALL": "Tutto", + "PORTRAIT": "Verticale", + "PORTRAIT_UP": "Verticale sopra", + "PORTRAIT_DOWN": "Verticale sotto", + "LANDSCAPE": "Orizzontale", + "LANDSCAPE_LEFT": "Orizzontale sinitra", + "LANDSCAPE_RIGHT": "Orizzontale destra", + "OTHER": "Altro", + "UNKNOWN": "Sconosciuto" + }, + "safe_area_in_controls": "Area sicura per i controlli", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostra i link del menu personalizzato", + "hide_libraries": "Nascondi Librerie", + "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", + "disable_haptic_feedback": "Disabilita il feedback aptico", + "default_quality": "Qualità predefinita" + }, + "downloads": { + "downloads_title": "Scaricamento", + "download_method": "Metodo per lo scaricamento", + "remux_max_download": "Numero di Remux da scaricare al massimo", + "auto_download": "Scaricamento automatico", + "optimized_versions_server": "Versioni del server di ottimizzazione", + "save_button": "Salva", + "optimized_server": "Server di ottimizzazione", + "optimized": "Ottimizzato", + "default": "Predefinito", + "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta" + }, + "plugins": { + "plugins_title": "Plugin", + "jellyseerr": { + "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", + "server_url": "URL del Server", + "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", + "server_url_placeholder": "URL di Jellyseerr...", + "password": "Password", + "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", + "save_button": "Salva", + "clear_button": "Cancella", + "login_button": "Accedi", + "total_media_requests": "Totale di richieste di media", + "movie_quota_limit": "Limite di quota per i film", + "movie_quota_days": "Giorni di quota per i film", + "tv_quota_limit": "Limite di quota per le serie TV", + "tv_quota_days": "Giorni di quota per le serie TV", + "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", + "unlimited": "Illimitato", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Abilita la ricerca Marlin ", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta", + "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_marlin": "Leggi di più su Marlin.", + "save_button": "Salva", + "toasts": { + "saved": "Salvato" + } + } + }, + "storage": { + "storage_title": "Spazio", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} di {{total}} usato", + "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" + }, + "intro": { + "show_intro": "Mostra intro", + "reset_intro": "Ripristina intro" + }, + "logs": { + "logs_title": "Log", + "no_logs_available": "Nessun log disponibile", + "delete_all_logs": "Cancella tutti i log" + }, + "languages": { + "title": "Lingue", + "app_language": "Lingua dell'App", + "app_language_description": "Selezione la lingua dell'app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Errore nella cancellazione dei file", + "background_downloads_enabled": "Scaricamento in background abilitato", + "background_downloads_disabled": "Scaricamento in background disabilitato", + "connected": "Connesso", + "could_not_connect": "Non è stato possibile connettersi", + "invalid_url": "URL invalido" + } + }, + "downloads": { + "downloads_title": "Scaricati", + "tvseries": "Serie TV", + "movies": "Film", + "queue": "Coda", + "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", + "no_items_in_queue": "Nessun elemento in coda", + "no_downloaded_items": "Nessun elemento scaricato", + "delete_all_movies_button": "Cancella tutti i film", + "delete_all_tvseries_button": "Cancella tutte le serie TV", + "delete_all_button": "Cancella tutti", + "active_download": "Scaricamento in corso", + "no_active_downloads": "Nessun scaricamento in corso", + "active_downloads": "Scaricamenti in corso", + "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", + "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", + "back": "Indietro", + "delete": "Cancella", + "something_went_wrong": "Qualcosa è andato storto", + "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodi", + "toasts": { + "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", + "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", + "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", + "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", + "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", + "download_cancelled": "Scaricamento annullato", + "could_not_cancel_download": "Impossibile annullare lo scaricamento", + "download_completed": "Scaricamento completato", + "download_started_for": "Scaricamento iniziato per {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", + "download_stated_for_item": "Scaricamento iniziato per {{item}}", + "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", + "download_completed_for_item": "Scaricamento completato per {{item}}", + "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", + "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", + "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", + "go_to_downloads": "Vai agli elementi scaricati" + } + } + }, + "search": { + "search_here": "Cerca qui...", + "search": "Cerca...", + "x_items": "{{count}} elementi", + "library": "Libreria", + "discover": "Scopri", + "no_results": "Nessun risultato", + "no_results_found_for": "Nessun risultato trovato per", + "movies": "Film", + "series": "Serie", + "episodes": "Episodi", + "collections": "Collezioni", + "actors": "Attori", + "request_movies": "Film Richiesti", + "request_series": "Serie Richieste", + "recently_added": "Aggiunti di Recente", + "recent_requests": "Richiesti di Recente", + "plex_watchlist": "Plex Watchlist", + "trending": "In tendenza", + "popular_movies": "Film Popolari", + "movie_genres": "Generi Film", + "upcoming_movies": "Film in arrivo", + "studios": "Studio", + "popular_tv": "Serie Popolari", + "tv_genres": "Generi Televisivi", + "upcoming_tv": "Serie in Arrivo", + "networks": "Network", + "tmdb_movie_keyword": "TMDB Parola chiave del film", + "tmdb_movie_genre": "TMDB Genere Film", + "tmdb_tv_keyword": "TMDB Parola chiave della serie", + "tmdb_tv_genre": "TMDB Genere Televisivo", + "tmdb_search": "TMDB Cerca", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", + "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" + }, + "library": { + "no_items_found": "Nessun elemento trovato", + "no_results": "Nessun risultato", + "no_libraries_found": "Nessuna libreria trovata", + "item_types": { + "movies": "film", + "series": "serie TV", + "boxsets": "cofanetti", + "items": "elementi" + }, + "options": { + "display": "Display", + "row": "Fila", + "list": "Lista", + "image_style": "Stile dell'immagine", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Mostra titoli", + "show_stats": "Mostra statistiche" + }, + "filters": { + "genres": "Generi", + "years": "Anni", + "sort_by": "Ordina per", + "sort_order": "Criterio di ordinamento", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tag" + } + }, + "favorites": { + "series": "Serie TV", + "movies": "Film", + "episodes": "Episodi", + "videos": "Video", + "boxsets": "Boxset", + "playlists": "Playlist", + "noDataTitle": "Ancora nessun preferito", + "noData": "Contrassegna gli elementi come preferiti per vederli apparire qui per un accesso rapido." + }, + "custom_links": { + "no_links": "Nessun link" + }, + "player": { + "error": "Errore", + "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", + "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", + "client_error": "Errore del client", + "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", + "message_from_server": "Messaggio dal server", + "video_has_finished_playing": "La riproduzione del video è terminata!", + "no_video_source": "Nessuna sorgente video...", + "next_episode": "Prossimo Episodio", + "refresh_tracks": "Aggiorna tracce", + "subtitle_tracks": "Tracce di sottotitoli:", + "audio_tracks": "Tracce audio:", + "playback_state": "Stato della riproduzione:", + "no_data_available": "Nessun dato disponibile", + "index": "Indice:" + }, + "item_card": { + "next_up": "Il prossimo", + "no_items_to_display": "Nessun elemento da visualizzare", + "cast_and_crew": "Cast e Equipaggio", + "series": "Serie", + "seasons": "Stagioni", + "season": "Stagione", + "no_episodes_for_this_season": "Nessun episodio per questa stagione", + "overview": "Panoramica", + "more_with": "Altri con {{name}}", + "similar_items": "Elementi simili", + "no_similar_items_found": "Non sono stati trovati elementi simili", + "video": "Video", + "more_details": "Più dettagli", + "quality": "Qualità", + "audio": "Audio", + "subtitles": "Sottotitoli", + "show_more": "Mostra di più", + "show_less": "Mostra di meno", + "appeared_in": "Apparso in", + "could_not_load_item": "Impossibile caricare l'elemento", + "none": "Nessuno", + "download": { + "download_season": "Scarica Stagione", + "download_series": "Scarica Serie", + "download_episode": "Scarica Episodio", + "download_movie": "Scarica Film", + "download_x_item": "Scarica {{item_count}} elementi", + "download_button": "Scarica", + "using_optimized_server": "Utilizzando il server di ottimizzazione", + "using_default_method": "Utilizzando il metodo predefinito" + } + }, + "live_tv": { + "next": "Prossimo", + "previous": "Precedente", + "live_tv": "TV in diretta", + "coming_soon": "Prossimamente", + "on_now": "In onda ora", + "shows": "Programmi", + "movies": "Film", + "sports": "Sport", + "for_kids": "Per Bambini", + "news": "Notiziari" + }, + "jellyseerr": { + "confirm": "Conferma", + "cancel": "Cancella", + "yes": "Si", + "whats_wrong": "Cosa c'è che non va?", + "issue_type": "Tipo di problema", + "select_an_issue": "Seleziona un problema", + "types": "Tipi", + "describe_the_issue": "(facoltativo) Descrivere il problema...", + "submit_button": "Invia", + "report_issue_button": "Segnalare il problema", + "request_button": "Richiedi", + "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", + "failed_to_login": "Accesso non riuscito", + "cast": "Cast", + "details": "Dettagli", + "status": "Stato", + "original_title": "Titolo originale", + "series_type": "Tipo di Serie", + "release_dates": "Date di Uscita", + "first_air_date": "Prima Data di Messa in Onda", + "next_air_date": "Prossima Data di Messa in Onda", + "revenue": "Ricavi", + "budget": "Budget", + "original_language": "Lingua Originale", + "production_country": "Paese di Produzione", + "studios": "Studio", + "network": "Network", + "currently_streaming_on": "Attualmente in streaming su", + "advanced": "Avanzate", + "request_as": "Richiedi Come", + "tags": "Tag", + "quality_profile": "Profilo qualità", + "root_folder": "Cartella radice", + "season_all": "Season (all)", + "season_number": "Stagione {{season_number}}", + "number_episodes": "{{episode_number}} Episodio", + "born": "Nato", + "appearances": "Aspetto", + "toasts": { + "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", + "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", + "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", + "issue_submitted": "Problema inviato!", + "requested_item": "Richiesto {{item}}!", + "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", + "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" + } + }, + "tabs": { + "home": "Home", + "search": "Cerca", + "library": "Libreria", + "custom_links": "Collegamenti personalizzati", + "favorites": "Preferiti" + } } diff --git a/translations/ja.json b/translations/ja.json index 44ac71bd..261b6724 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "ユーザー名は必須です", - "error_title": "エラー", - "login_title": "ログイン", - "login_to_title": "ログイン先", - "username_placeholder": "ユーザー名", - "password_placeholder": "パスワード", - "login_button": "ログイン", - "quick_connect": "クイックコネクト", - "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", - "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", - "got_it": "了解", - "connection_failed": "接続に失敗しました", - "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", - "an_unexpected_error_occured": "予期しないエラーが発生しました", - "change_server": "サーバーの変更", - "invalid_username_or_password": "ユーザー名またはパスワードが無効です", - "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", - "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", - "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", - "there_is_a_server_error": "サーバーエラーが発生しました", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" - }, - "server": { - "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "接続", - "previous_servers": "前のサーバー", - "clear_button": "クリア", - "search_for_local_servers": "ローカルサーバーを検索", - "searching": "検索中...", - "servers": "サーバー" - }, - "home": { - "no_internet": "インターネット接続がありません", - "no_items": "アイテムはありません", - "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", - "go_to_downloads": "ダウンロードに移動", - "oops": "おっと!", - "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", - "continue_watching": "続きを見る", - "next_up": "次の動画", - "recently_added_in": "{{libraryName}}に最近追加された", - "suggested_movies": "おすすめ映画", - "suggested_episodes": "おすすめエピソード", - "intro": { - "welcome_to_streamyfin": "Streamyfinへようこそ", - "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", - "features_title": "特長", - "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", - "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", - "downloads_feature_title": "ダウンロード", - "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", - "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", - "centralised_settings_plugin_title": "集中設定プラグイン", - "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", - "done_button": "完了", - "go_to_settings_button": "設定に移動", - "read_more": "続きを読む" - }, - "settings": { - "settings_title": "設定", - "log_out_button": "ログアウト", - "user_info": { - "user_info_title": "ユーザー情報", - "user": "ユーザー", - "server": "サーバー", - "token": "トークン", - "app_version": "アプリバージョン" - }, - "quick_connect": { - "quick_connect_title": "クイックコネクト", - "authorize_button": "クイックコネクトを承認する", - "enter_the_quick_connect_code": "クイックコネクトコードを入力...", - "success": "成功しました", - "quick_connect_autorized": "クイックコネクトが承認されました", - "error": "エラー", - "invalid_code": "無効なコードです", - "authorize": "承認" - }, - "media_controls": { - "media_controls_title": "メディアコントロール", - "forward_skip_length": "スキップの長さ", - "rewind_length": "巻き戻しの長さ", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "オーディオ", - "set_audio_track": "前のアイテムからオーディオトラックを設定", - "audio_language": "オーディオ言語", - "audio_hint": "デフォルトのオーディオ言語を選択します。", - "none": "なし", - "language": "言語" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕の言語", - "subtitle_mode": "字幕モード", - "set_subtitle_track": "前のアイテムから字幕トラックを設定", - "subtitle_size": "字幕サイズ", - "subtitle_hint": "字幕設定を構成します。", - "none": "なし", - "language": "言語", - "loading": "ロード中", - "modes": { - "Default": "デフォルト", - "Smart": "スマート", - "Always": "常に", - "None": "なし", - "OnlyForced": "強制のみ" - } - }, - "other": { - "other_title": "その他", - "follow_device_orientation": "画面の自動回転", - "video_orientation": "動画の向き", - "orientation": "向き", - "orientations": { - "DEFAULT": "デフォルト", - "ALL": "すべて", - "PORTRAIT": "縦", - "PORTRAIT_UP": "縦向き(上)", - "PORTRAIT_DOWN": "縦方向", - "LANDSCAPE": "横方向", - "LANDSCAPE_LEFT": "横方向 左", - "LANDSCAPE_RIGHT": "横方向 右", - "OTHER": "その他", - "UNKNOWN": "不明" - }, - "safe_area_in_controls": "コントロールの安全エリア", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "カスタムメニューのリンクを表示", - "hide_libraries": "ライブラリを非表示", - "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", - "disable_haptic_feedback": "触覚フィードバックを無効にする" - }, - "downloads": { - "downloads_title": "ダウンロード", - "download_method": "ダウンロード方法", - "remux_max_download": "Remux最大ダウンロード数", - "auto_download": "自動ダウンロード", - "optimized_versions_server": "Optimized versionsサーバー", - "save_button": "保存", - "optimized_server": "Optimizedサーバー", - "optimized": "最適化", - "default": "デフォルト", - "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート" - }, - "plugins": { - "plugins_title": "プラグイン", - "jellyseerr": { - "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", - "server_url": "サーバーURL", - "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "パスワード", - "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", - "save_button": "保存", - "clear_button": "クリア", - "login_button": "ログイン", - "total_media_requests": "メディアリクエストの合計", - "movie_quota_limit": "映画のクオータ制限", - "movie_quota_days": "映画のクオータ日数", - "tv_quota_limit": "テレビのクオータ制限", - "tv_quota_days": "テレビのクオータ日数", - "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", - "unlimited": "無制限", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "マーリン検索を有効にする ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート", - "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_marlin": "Marlinについて詳しく読む。", - "save_button": "保存", - "toasts": { - "saved": "保存しました" - } - } - }, - "storage": { - "storage_title": "ストレージ", - "app_usage": "アプリ {{usedSpace}}%", - "phone_usage": "電話 {{availableSpace}}%", - "size_used": "{{used}} / {{total}} 使用済み", - "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" - }, - "intro": { - "show_intro": "イントロを表示", - "reset_intro": "イントロをリセット" - }, - "logs": { - "logs_title": "ログ", - "no_logs_available": "ログがありません", - "delete_all_logs": "すべてのログを削除" - }, - "languages": { - "title": "言語", - "app_language": "アプリの言語", - "app_language_description": "アプリの言語を選択。", - "system": "システム" - }, - "toasts": { - "error_deleting_files": "ファイルの削除エラー", - "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", - "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", - "connected": "接続済み", - "could_not_connect": "接続できません", - "invalid_url": "無効なURL" - } - }, - "downloads": { - "downloads_title": "ダウンロード", - "tvseries": "TVシリーズ", - "movies": "映画", - "queue": "キュー", - "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", - "no_items_in_queue": "キューにアイテムがありません", - "no_downloaded_items": "ダウンロードしたアイテムはありません", - "delete_all_movies_button": "すべての映画を削除", - "delete_all_tvseries_button": "すべてのシリーズを削除", - "delete_all_button": "すべて削除", - "active_download": "アクティブなダウンロード", - "no_active_downloads": "アクティブなダウンロードはありません", - "active_downloads": "アクティブなダウンロード", - "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", - "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", - "back": "戻る", - "delete": "削除", - "something_went_wrong": "問題が発生しました", - "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", - "eta": "ETA {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", - "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", - "failed_to_delete_all_movies": "すべての映画を削除できませんでした", - "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", - "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", - "download_cancelled": "ダウンロードをキャンセルしました", - "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", - "download_completed": "ダウンロードが完了しました", - "download_started_for": "{{item}}のダウンロードが開始されました", - "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", - "download_stated_for_item": "{{item}}のダウンロードが開始されました", - "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", - "download_completed_for_item": "{{item}}のダウンロードが完了しました", - "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", - "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", - "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", - "no_response_received_from_server": "サーバーからの応答がありません", - "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", - "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", - "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", - "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", - "go_to_downloads": "ダウンロードに移動" - } - } - }, - "search": { - "search_here": "ここを検索...", - "search": "検索...", - "x_items": "{{count}}のアイテム", - "library": "ライブラリ", - "discover": "見つける", - "no_results": "結果はありません", - "no_results_found_for": "結果が見つかりませんでした:", - "movies": "映画", - "series": "シリーズ", - "episodes": "エピソード", - "collections": "コレクション", - "actors": "俳優", - "request_movies": "映画をリクエスト", - "request_series": "シリーズをリクエスト", - "recently_added": "最近の追加", - "recent_requests": "最近のリクエスト", - "plex_watchlist": "Plexウォッチリスト", - "trending": "トレンド", - "popular_movies": "人気の映画", - "movie_genres": "映画のジャンル", - "upcoming_movies": "今後リリースされる映画", - "studios": "制作会社", - "popular_tv": "人気のテレビ番組", - "tv_genres": "シリーズのジャンル", - "upcoming_tv": "今後リリースされるシリーズ", - "networks": "ネットワーク", - "tmdb_movie_keyword": "TMDB映画キーワード", - "tmdb_movie_genre": "TMDB映画ジャンル", - "tmdb_tv_keyword": "TMDBシリーズキーワード", - "tmdb_tv_genre": "TMDBシリーズジャンル", - "tmdb_search": "TMDB検索", - "tmdb_studio": "TMDB 制作会社", - "tmdb_network": "TMDB ネットワーク", - "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", - "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" - }, - "library": { - "no_items_found": "アイテムが見つかりません", - "no_results": "検索結果はありません", - "no_libraries_found": "ライブラリが見つかりません", - "item_types": { - "movies": "映画", - "series": "シリーズ", - "boxsets": "ボックスセット", - "items": "アイテム" - }, - "options": { - "display": "表示", - "row": "行", - "list": "リスト", - "image_style": "画像のスタイル", - "poster": "ポスター", - "cover": "カバー", - "show_titles": "タイトルの表示", - "show_stats": "統計を表示" - }, - "filters": { - "genres": "ジャンル", - "years": "年", - "sort_by": "ソート", - "sort_order": "ソート順", - "asc": "Ascending", - "desc": "Descending", - "tags": "タグ" - } - }, - "favorites": { - "series": "シリーズ", - "movies": "映画", - "episodes": "エピソード", - "videos": "ビデオ", - "boxsets": "ボックスセット", - "playlists": "プレイリスト", - "noDataTitle": "お気に入りはまだありません", - "noData": "アイテムをお気に入りとしてマークすると、ここに表示されクイックアクセスできるようになります。" - }, - "custom_links": { - "no_links": "リンクがありません" - }, - "player": { - "error": "エラー", - "failed_to_get_stream_url": "ストリームURLを取得できませんでした", - "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", - "client_error": "クライアントエラー", - "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", - "message_from_server": "サーバーからのメッセージ", - "video_has_finished_playing": "ビデオの再生が終了しました!", - "no_video_source": "動画ソースがありません...", - "next_episode": "次のエピソード", - "refresh_tracks": "トラックを更新", - "subtitle_tracks": "字幕トラック:", - "audio_tracks": "音声トラック:", - "playback_state": "再生状態:", - "no_data_available": "データなし", - "index": "インデックス:" - }, - "item_card": { - "next_up": "次", - "no_items_to_display": "表示するアイテムがありません", - "cast_and_crew": "キャスト&クルー", - "series": "シリーズ", - "seasons": "シーズン", - "season": "シーズン", - "no_episodes_for_this_season": "このシーズンのエピソードはありません", - "overview": "ストーリー", - "more_with": "{{name}}の詳細", - "similar_items": "類似アイテム", - "no_similar_items_found": "類似のアイテムは見つかりませんでした", - "video": "映像", - "more_details": "さらに詳細を表示", - "quality": "画質", - "audio": "音声", - "subtitles": "字幕", - "show_more": "もっと見る", - "show_less": "少なく表示", - "appeared_in": "出演作品", - "could_not_load_item": "アイテムを読み込めませんでした", - "none": "なし", - "download": { - "download_season": "シーズンをダウンロード", - "download_series": "シリーズをダウンロード", - "download_episode": "エピソードをダウンロード", - "download_movie": "映画をダウンロード", - "download_x_item": "{{item_count}}のアイテムをダウンロード", - "download_button": "ダウンロード", - "using_optimized_server": "Optimizeサーバーを使用する", - "using_default_method": "デフォルトの方法を使用" - } - }, - "live_tv": { - "next": "次", - "previous": "前", - "live_tv": "ライブTV", - "coming_soon": "近日公開", - "on_now": "現在", - "shows": "表示", - "movies": "映画", - "sports": "スポーツ", - "for_kids": "子供向け", - "news": "ニュース" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "キャンセル", - "yes": "はい", - "whats_wrong": "どうしましたか?", - "issue_type": "問題の種類", - "select_an_issue": "問題を選択", - "types": "種類", - "describe_the_issue": "(オプション) 問題を説明してください...", - "submit_button": "送信", - "report_issue_button": "チケットを報告", - "request_button": "リクエスト", - "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", - "failed_to_login": "ログインに失敗しました", - "cast": "出演者", - "details": "詳細", - "status": "状態", - "original_title": "原題", - "series_type": "シリーズタイプ", - "release_dates": "公開日", - "first_air_date": "初放送日", - "next_air_date": "次回放送日", - "revenue": "収益", - "budget": "予算", - "original_language": "オリジナルの言語", - "production_country": "制作国", - "studios": "制作会社", - "network": "ネットワーク", - "currently_streaming_on": "ストリーミング中", - "advanced": "詳細", - "request_as": "別ユーザーとしてリクエスト", - "tags": "タグ", - "quality_profile": "画質プロファイル", - "root_folder": "ルートフォルダ", - "season_all": "Season (all)", - "season_number": "シーズン{{season_number}}", - "number_episodes": "エピソード{{episode_number}}", - "born": "生まれ", - "appearances": "出演", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", - "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", - "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", - "issue_submitted": "チケットを送信しました!", - "requested_item": "{{item}}をリクエスト!", - "you_dont_have_permission_to_request": "リクエストする権限がありません!", - "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" - } - }, - "tabs": { - "home": "ホーム", - "search": "検索", - "library": "ライブラリ", - "custom_links": "カスタムリンク", - "favorites": "お気に入り" - } + "login": { + "username_required": "ユーザー名は必須です", + "error_title": "エラー", + "login_title": "ログイン", + "login_to_title": "ログイン先", + "username_placeholder": "ユーザー名", + "password_placeholder": "パスワード", + "login_button": "ログイン", + "quick_connect": "クイックコネクト", + "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", + "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", + "got_it": "了解", + "connection_failed": "接続に失敗しました", + "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", + "an_unexpected_error_occured": "予期しないエラーが発生しました", + "change_server": "サーバーの変更", + "invalid_username_or_password": "ユーザー名またはパスワードが無効です", + "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", + "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", + "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", + "there_is_a_server_error": "サーバーエラーが発生しました", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" + }, + "server": { + "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "接続", + "previous_servers": "前のサーバー", + "clear_button": "クリア", + "search_for_local_servers": "ローカルサーバーを検索", + "searching": "検索中...", + "servers": "サーバー" + }, + "home": { + "no_internet": "インターネット接続がありません", + "no_items": "アイテムはありません", + "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", + "go_to_downloads": "ダウンロードに移動", + "oops": "おっと!", + "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", + "continue_watching": "続きを見る", + "next_up": "次の動画", + "recently_added_in": "{{libraryName}}に最近追加された", + "suggested_movies": "おすすめ映画", + "suggested_episodes": "おすすめエピソード", + "intro": { + "welcome_to_streamyfin": "Streamyfinへようこそ", + "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", + "features_title": "特長", + "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", + "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", + "downloads_feature_title": "ダウンロード", + "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", + "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", + "centralised_settings_plugin_title": "集中設定プラグイン", + "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", + "done_button": "完了", + "go_to_settings_button": "設定に移動", + "read_more": "続きを読む" + }, + "settings": { + "settings_title": "設定", + "log_out_button": "ログアウト", + "user_info": { + "user_info_title": "ユーザー情報", + "user": "ユーザー", + "server": "サーバー", + "token": "トークン", + "app_version": "アプリバージョン" + }, + "quick_connect": { + "quick_connect_title": "クイックコネクト", + "authorize_button": "クイックコネクトを承認する", + "enter_the_quick_connect_code": "クイックコネクトコードを入力...", + "success": "成功しました", + "quick_connect_autorized": "クイックコネクトが承認されました", + "error": "エラー", + "invalid_code": "無効なコードです", + "authorize": "承認" + }, + "media_controls": { + "media_controls_title": "メディアコントロール", + "forward_skip_length": "スキップの長さ", + "rewind_length": "巻き戻しの長さ", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "オーディオ", + "set_audio_track": "前のアイテムからオーディオトラックを設定", + "audio_language": "オーディオ言語", + "audio_hint": "デフォルトのオーディオ言語を選択します。", + "none": "なし", + "language": "言語" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕の言語", + "subtitle_mode": "字幕モード", + "set_subtitle_track": "前のアイテムから字幕トラックを設定", + "subtitle_size": "字幕サイズ", + "subtitle_hint": "字幕設定を構成します。", + "none": "なし", + "language": "言語", + "loading": "ロード中", + "modes": { + "Default": "デフォルト", + "Smart": "スマート", + "Always": "常に", + "None": "なし", + "OnlyForced": "強制のみ" + } + }, + "other": { + "other_title": "その他", + "follow_device_orientation": "画面の自動回転", + "video_orientation": "動画の向き", + "orientation": "向き", + "orientations": { + "DEFAULT": "デフォルト", + "ALL": "すべて", + "PORTRAIT": "縦", + "PORTRAIT_UP": "縦向き(上)", + "PORTRAIT_DOWN": "縦方向", + "LANDSCAPE": "横方向", + "LANDSCAPE_LEFT": "横方向 左", + "LANDSCAPE_RIGHT": "横方向 右", + "OTHER": "その他", + "UNKNOWN": "不明" + }, + "safe_area_in_controls": "コントロールの安全エリア", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "カスタムメニューのリンクを表示", + "hide_libraries": "ライブラリを非表示", + "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", + "disable_haptic_feedback": "触覚フィードバックを無効にする" + }, + "downloads": { + "downloads_title": "ダウンロード", + "download_method": "ダウンロード方法", + "remux_max_download": "Remux最大ダウンロード数", + "auto_download": "自動ダウンロード", + "optimized_versions_server": "Optimized versionsサーバー", + "save_button": "保存", + "optimized_server": "Optimizedサーバー", + "optimized": "最適化", + "default": "デフォルト", + "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート" + }, + "plugins": { + "plugins_title": "プラグイン", + "jellyseerr": { + "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", + "server_url": "サーバーURL", + "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "パスワード", + "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", + "save_button": "保存", + "clear_button": "クリア", + "login_button": "ログイン", + "total_media_requests": "メディアリクエストの合計", + "movie_quota_limit": "映画のクオータ制限", + "movie_quota_days": "映画のクオータ日数", + "tv_quota_limit": "テレビのクオータ制限", + "tv_quota_days": "テレビのクオータ日数", + "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", + "unlimited": "無制限", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "マーリン検索を有効にする ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート", + "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_marlin": "Marlinについて詳しく読む。", + "save_button": "保存", + "toasts": { + "saved": "保存しました" + } + } + }, + "storage": { + "storage_title": "ストレージ", + "app_usage": "アプリ {{usedSpace}}%", + "phone_usage": "電話 {{availableSpace}}%", + "size_used": "{{used}} / {{total}} 使用済み", + "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" + }, + "intro": { + "show_intro": "イントロを表示", + "reset_intro": "イントロをリセット" + }, + "logs": { + "logs_title": "ログ", + "no_logs_available": "ログがありません", + "delete_all_logs": "すべてのログを削除" + }, + "languages": { + "title": "言語", + "app_language": "アプリの言語", + "app_language_description": "アプリの言語を選択。", + "system": "システム" + }, + "toasts": { + "error_deleting_files": "ファイルの削除エラー", + "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", + "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", + "connected": "接続済み", + "could_not_connect": "接続できません", + "invalid_url": "無効なURL" + } + }, + "downloads": { + "downloads_title": "ダウンロード", + "tvseries": "TVシリーズ", + "movies": "映画", + "queue": "キュー", + "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", + "no_items_in_queue": "キューにアイテムがありません", + "no_downloaded_items": "ダウンロードしたアイテムはありません", + "delete_all_movies_button": "すべての映画を削除", + "delete_all_tvseries_button": "すべてのシリーズを削除", + "delete_all_button": "すべて削除", + "active_download": "アクティブなダウンロード", + "no_active_downloads": "アクティブなダウンロードはありません", + "active_downloads": "アクティブなダウンロード", + "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", + "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", + "back": "戻る", + "delete": "削除", + "something_went_wrong": "問題が発生しました", + "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", + "eta": "ETA {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", + "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", + "failed_to_delete_all_movies": "すべての映画を削除できませんでした", + "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", + "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", + "download_cancelled": "ダウンロードをキャンセルしました", + "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", + "download_completed": "ダウンロードが完了しました", + "download_started_for": "{{item}}のダウンロードが開始されました", + "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", + "download_stated_for_item": "{{item}}のダウンロードが開始されました", + "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", + "download_completed_for_item": "{{item}}のダウンロードが完了しました", + "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", + "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", + "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", + "no_response_received_from_server": "サーバーからの応答がありません", + "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", + "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", + "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", + "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", + "go_to_downloads": "ダウンロードに移動" + } + } + }, + "search": { + "search_here": "ここを検索...", + "search": "検索...", + "x_items": "{{count}}のアイテム", + "library": "ライブラリ", + "discover": "見つける", + "no_results": "結果はありません", + "no_results_found_for": "結果が見つかりませんでした:", + "movies": "映画", + "series": "シリーズ", + "episodes": "エピソード", + "collections": "コレクション", + "actors": "俳優", + "request_movies": "映画をリクエスト", + "request_series": "シリーズをリクエスト", + "recently_added": "最近の追加", + "recent_requests": "最近のリクエスト", + "plex_watchlist": "Plexウォッチリスト", + "trending": "トレンド", + "popular_movies": "人気の映画", + "movie_genres": "映画のジャンル", + "upcoming_movies": "今後リリースされる映画", + "studios": "制作会社", + "popular_tv": "人気のテレビ番組", + "tv_genres": "シリーズのジャンル", + "upcoming_tv": "今後リリースされるシリーズ", + "networks": "ネットワーク", + "tmdb_movie_keyword": "TMDB映画キーワード", + "tmdb_movie_genre": "TMDB映画ジャンル", + "tmdb_tv_keyword": "TMDBシリーズキーワード", + "tmdb_tv_genre": "TMDBシリーズジャンル", + "tmdb_search": "TMDB検索", + "tmdb_studio": "TMDB 制作会社", + "tmdb_network": "TMDB ネットワーク", + "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", + "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" + }, + "library": { + "no_items_found": "アイテムが見つかりません", + "no_results": "検索結果はありません", + "no_libraries_found": "ライブラリが見つかりません", + "item_types": { + "movies": "映画", + "series": "シリーズ", + "boxsets": "ボックスセット", + "items": "アイテム" + }, + "options": { + "display": "表示", + "row": "行", + "list": "リスト", + "image_style": "画像のスタイル", + "poster": "ポスター", + "cover": "カバー", + "show_titles": "タイトルの表示", + "show_stats": "統計を表示" + }, + "filters": { + "genres": "ジャンル", + "years": "年", + "sort_by": "ソート", + "sort_order": "ソート順", + "asc": "Ascending", + "desc": "Descending", + "tags": "タグ" + } + }, + "favorites": { + "series": "シリーズ", + "movies": "映画", + "episodes": "エピソード", + "videos": "ビデオ", + "boxsets": "ボックスセット", + "playlists": "プレイリスト", + "noDataTitle": "お気に入りはまだありません", + "noData": "アイテムをお気に入りとしてマークすると、ここに表示されクイックアクセスできるようになります。" + }, + "custom_links": { + "no_links": "リンクがありません" + }, + "player": { + "error": "エラー", + "failed_to_get_stream_url": "ストリームURLを取得できませんでした", + "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", + "client_error": "クライアントエラー", + "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", + "message_from_server": "サーバーからのメッセージ", + "video_has_finished_playing": "ビデオの再生が終了しました!", + "no_video_source": "動画ソースがありません...", + "next_episode": "次のエピソード", + "refresh_tracks": "トラックを更新", + "subtitle_tracks": "字幕トラック:", + "audio_tracks": "音声トラック:", + "playback_state": "再生状態:", + "no_data_available": "データなし", + "index": "インデックス:" + }, + "item_card": { + "next_up": "次", + "no_items_to_display": "表示するアイテムがありません", + "cast_and_crew": "キャスト&クルー", + "series": "シリーズ", + "seasons": "シーズン", + "season": "シーズン", + "no_episodes_for_this_season": "このシーズンのエピソードはありません", + "overview": "ストーリー", + "more_with": "{{name}}の詳細", + "similar_items": "類似アイテム", + "no_similar_items_found": "類似のアイテムは見つかりませんでした", + "video": "映像", + "more_details": "さらに詳細を表示", + "quality": "画質", + "audio": "音声", + "subtitles": "字幕", + "show_more": "もっと見る", + "show_less": "少なく表示", + "appeared_in": "出演作品", + "could_not_load_item": "アイテムを読み込めませんでした", + "none": "なし", + "download": { + "download_season": "シーズンをダウンロード", + "download_series": "シリーズをダウンロード", + "download_episode": "エピソードをダウンロード", + "download_movie": "映画をダウンロード", + "download_x_item": "{{item_count}}のアイテムをダウンロード", + "download_button": "ダウンロード", + "using_optimized_server": "Optimizeサーバーを使用する", + "using_default_method": "デフォルトの方法を使用" + } + }, + "live_tv": { + "next": "次", + "previous": "前", + "live_tv": "ライブTV", + "coming_soon": "近日公開", + "on_now": "現在", + "shows": "表示", + "movies": "映画", + "sports": "スポーツ", + "for_kids": "子供向け", + "news": "ニュース" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "キャンセル", + "yes": "はい", + "whats_wrong": "どうしましたか?", + "issue_type": "問題の種類", + "select_an_issue": "問題を選択", + "types": "種類", + "describe_the_issue": "(オプション) 問題を説明してください...", + "submit_button": "送信", + "report_issue_button": "チケットを報告", + "request_button": "リクエスト", + "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", + "failed_to_login": "ログインに失敗しました", + "cast": "出演者", + "details": "詳細", + "status": "状態", + "original_title": "原題", + "series_type": "シリーズタイプ", + "release_dates": "公開日", + "first_air_date": "初放送日", + "next_air_date": "次回放送日", + "revenue": "収益", + "budget": "予算", + "original_language": "オリジナルの言語", + "production_country": "制作国", + "studios": "制作会社", + "network": "ネットワーク", + "currently_streaming_on": "ストリーミング中", + "advanced": "詳細", + "request_as": "別ユーザーとしてリクエスト", + "tags": "タグ", + "quality_profile": "画質プロファイル", + "root_folder": "ルートフォルダ", + "season_all": "Season (all)", + "season_number": "シーズン{{season_number}}", + "number_episodes": "エピソード{{episode_number}}", + "born": "生まれ", + "appearances": "出演", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", + "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", + "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", + "issue_submitted": "チケットを送信しました!", + "requested_item": "{{item}}をリクエスト!", + "you_dont_have_permission_to_request": "リクエストする権限がありません!", + "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" + } + }, + "tabs": { + "home": "ホーム", + "search": "検索", + "library": "ライブラリ", + "custom_links": "カスタムリンク", + "favorites": "お気に入り" + } } diff --git a/translations/nl.json b/translations/nl.json index 039803cc..eb213102 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Gebruikersnaam is verplicht", - "error_title": "Fout", - "login_title": "Aanmelden", - "login_to_title": "Aanmelden bij", - "username_placeholder": "Gebruikersnaam", - "password_placeholder": "Wachtwoord", - "login_button": "Aanmelden", - "quick_connect": "Snel Verbinden", - "enter_code_to_login": "Vul code {{code}} in om aan te melden", - "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", - "got_it": "Begrepen", - "connection_failed": "Verbinding mislukt", - "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", - "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", - "change_server": "Server wijzigen", - "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", - "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", - "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", - "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", - "there_is_a_server_error": "Er is een serverfout", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" - }, - "server": { - "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", - "server_url_placeholder": "http(s)://je-server.com", - "connect_button": "Verbinden", - "previous_servers": "vorige servers", - "clear_button": "Wissen", - "search_for_local_servers": "Zoek naar lokale servers", - "searching": "Zoeken...", - "servers": "Servers" - }, - "home": { - "no_internet": "Geen Internet", - "no_items": "Geen items", - "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", - "go_to_downloads": "Ga naar downloads", - "oops": "Oeps!", - "error_message": "Er ging iets fout\nGelieve af en aan te melden.", - "continue_watching": "Verder Kijken", - "next_up": "Volgende", - "recently_added_in": "Recent toegevoegd in {{libraryName}}", - "suggested_movies": "Voorgestelde films", - "suggested_episodes": "Voorgestelde Afleveringen", - "intro": { - "welcome_to_streamyfin": "Welkom bij Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", - "features_title": "Functies", - "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", - "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", - "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", - "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", - "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", - "done_button": "Gedaan", - "go_to_settings_button": "Ga naar instellingen", - "read_more": "Lees meer" - }, - "settings": { - "settings_title": "Instellingen", - "log_out_button": "Afmelden", - "user_info": { - "user_info_title": "Gebruiker Info", - "user": "Gebruiker", - "server": "Server", - "token": "Token", - "app_version": "App Versie" - }, - "quick_connect": { - "quick_connect_title": "Snel Verbinden", - "authorize_button": "Snel Verbinden toestaan", - "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", - "success": "Succes", - "quick_connect_autorized": "Snel Verbinden toegestaan", - "error": "Fout", - "invalid_code": "Ongeldige code", - "authorize": "Toestaan" - }, - "media_controls": { - "media_controls_title": "Media Bedieningen", - "forward_skip_length": "Duur voorwaarts overslaan", - "rewind_length": "Duur terugspoelen", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Gebruik Audio Track Van Vorig Item", - "audio_language": "Audio taal", - "audio_hint": "Kies een standaard audio taal.", - "none": "Geen", - "language": "Taal" - }, - "subtitles": { - "subtitle_title": "Ondertitels", - "subtitle_language": "Ondertitel taal", - "subtitle_mode": "Ondertitelmodus", - "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", - "subtitle_size": "Ondertitel Grootte", - "subtitle_hint": "Stel ondertitel voorkeuren in.", - "none": "Geen", - "language": "Taal", - "loading": "Laden", - "modes": { - "Default": "Standaard", - "Smart": "Slim", - "Always": "Altijd", - "None": "Geen", - "OnlyForced": "Alleen Geforceerd" - } - }, - "other": { - "other_title": "Andere", - "follow_device_orientation": "Automatisch draaien", - "video_orientation": "Video oriëntatie", - "orientation": "Oriëntatie", - "orientations": { - "DEFAULT": "Standaard", - "ALL": "Alle", - "PORTRAIT": "Portret", - "PORTRAIT_UP": "Portret Omhoog", - "PORTRAIT_DOWN": "Portret Omlaag", - "LANDSCAPE": "Landschap", - "LANDSCAPE_LEFT": "Landschap Links", - "LANDSCAPE_RIGHT": "Landschap Rechts", - "OTHER": "Andere", - "UNKNOWN": "Onbekend" - }, - "safe_area_in_controls": "Veilig gebied in bedieningen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Aangepaste menulinks tonen", - "hide_libraries": "Verberg Bibliotheken", - "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", - "disable_haptic_feedback": "Haptische feedback uitschakelen", - "default_quality": "Standaard kwaliteit" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download methode", - "remux_max_download": "Maximale Remux-download", - "auto_download": "Auto download", - "optimized_versions_server": "Geoptimaliseerde server versies", - "save_button": "Opslaan", - "optimized_server": "Geoptimaliseerde Server", - "optimized": "Geoptimaliseerd", - "default": "Standaard", - "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", - "server_url": "Server URL", - "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Wachtwoord", - "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", - "save_button": "Opslaan", - "clear_button": "Wissen", - "login_button": "Aanmelden", - "total_media_requests": "Totaal aantal mediaverzoeken", - "movie_quota_limit": "Limiet filmquota", - "movie_quota_days": "Filmquota dagen", - "tv_quota_limit": "Limiet serie quota", - "tv_quota_days": "Serie Quota dagen", - "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", - "unlimited": "Ongelimiteerd", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Search inschakelen ", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort", - "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_marlin": "Lees meer over Marlin.", - "save_button": "Opslaan", - "toasts": { - "saved": "Opgeslagen" - } - } - }, - "storage": { - "storage_title": "Opslag", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Toestel {{availableSpace}}%", - "size_used": "{{used}} van {{total}} gebruikt", - "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" - }, - "intro": { - "show_intro": "Toon intro", - "reset_intro": "intro opnieuw instellen" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Geen logs beschikbaar", - "delete_all_logs": "Verwijder alle logs" - }, - "languages": { - "title": "Talen", - "app_language": "App taal", - "app_language_description": "Selecteer een taal voor de app.", - "system": "Systeem" - }, - "toasts": { - "error_deleting_files": "Fout bij het verwijderen van bestanden", - "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", - "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", - "connected": "Verbonden", - "could_not_connect": "Kon niet verbinden", - "invalid_url": "Ongeldige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "Series", - "movies": "Films", - "queue": "Wachtrij", - "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", - "no_items_in_queue": "Geen items in wachtrij", - "no_downloaded_items": "Geen gedownloade items", - "delete_all_movies_button": "Verwijder alle films", - "delete_all_tvseries_button": "Verwijder alle Series", - "delete_all_button": "Verwijder alles", - "active_download": "Actieve download", - "no_active_downloads": "Geen actieve downloads", - "active_downloads": "Actieve downloads", - "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", - "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", - "back": "Terug", - "delete": "Verwijder", - "something_went_wrong": "Er ging iets mis", - "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", - "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", - "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", - "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", - "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", - "download_cancelled": "Download geannuleerd", - "could_not_cancel_download": "Kon de download niet annuleren", - "download_completed": "Download afgerond", - "download_started_for": "Download gestart voor {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", - "download_stated_for_item": "Download gestart voor {{item}}", - "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", - "download_completed_for_item": "Download afgerond voor {{item}}", - "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", - "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", - "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", - "no_response_received_from_server": "Geen antwoord gekregen van de server", - "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", - "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", - "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", - "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", - "go_to_downloads": "Ga naar downloads" - } - } - }, - "search": { - "search_here": "Zoek hier...", - "search": "Zoek...", - "x_items": "{{count}} items", - "library": "Bibliotheek", - "discover": "Ontdek", - "no_results": "Geen resultaten", - "no_results_found_for": "Geen resultaten gevonden voor", - "movies": "Films", - "series": "Series", - "episodes": "Afleveringen", - "collections": "Collecties", - "actors": "Acteurs", - "request_movies": "Vraag films aan", - "request_series": "Vraag series aan", - "recently_added": "Recent Toegevoegd", - "recent_requests": "Recent Aangevraagd", - "plex_watchlist": "Plex Kijklijst", - "trending": "Trending", - "popular_movies": "Populaire films", - "movie_genres": "Film Genres", - "upcoming_movies": "Aankomende films", - "studios": "Studios", - "popular_tv": "Populaire TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Aankomende TV", - "networks": "Netwerken", - "tmdb_movie_keyword": "TMDB Film Trefwoord", - "tmdb_movie_genre": "TMDB Filmgenres", - "tmdb_tv_keyword": "TMDB TV Trefwoord", - "tmdb_tv_genre": "TMDB TV-Genres", - "tmdb_search": "TMDB Zoeken", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netwerk", - "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", - "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" - }, - "library": { - "no_items_found": "Geen items gevonden", - "no_results": "Geen resultaten", - "no_libraries_found": "Geen bibliotheken gevonden", - "item_types": { - "movies": "Films", - "series": "Series", - "boxsets": "Boxsets", - "items": "items" - }, - "options": { - "display": "Weergave", - "row": "Rij", - "list": "Lijst", - "image_style": "Stijl van afbeelding", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Toon titels", - "show_stats": "Toon statistieken" - }, - "filters": { - "genres": "Genres", - "years": "Jaren", - "sort_by": "Sorteren op", - "sort_order": "Sorteer volgorde", - "asc": "Ascending", - "desc": "Descending", - "tags": "Labels" - } - }, - "favorites": { - "series": "Series", - "movies": "Films", - "episodes": "Afleveringen", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Afspeellijsten", - "noDataTitle": "Nog geen favorieten", - "noData": "Markeer items als favoriet om ze hier te laten verschijnen voor snelle toegang." - }, - "custom_links": { - "no_links": "Geen links" - }, - "player": { - "error": "Fout", - "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", - "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", - "client_error": "Fout van de client", - "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", - "message_from_server": "Bericht van de server", - "video_has_finished_playing": "Video is gedaan met spelen!", - "no_video_source": "Geen videobron...", - "next_episode": "Volgende Aflevering", - "refresh_tracks": "Tracks verversen", - "subtitle_tracks": "Ondertitel Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Afspeelstatus:", - "no_data_available": "Geen data beschikbaar", - "index": "Index:" - }, - "item_card": { - "next_up": "Volgende", - "no_items_to_display": "Geen items om te tonen", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seizoenen", - "season": "Seizoen", - "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", - "overview": "Overzicht", - "more_with": "Meer met {{name}}", - "similar_items": "Gelijkaardige items", - "no_similar_items_found": "Geen gelijkaardige items gevonden", - "video": "Video", - "more_details": "Meer details", - "quality": "Kwaliteit", - "audio": "Audio", - "subtitles": "Ondertitel", - "show_more": "Toon meer", - "show_less": "Toon minder", - "appeared_in": "Verschenen in", - "could_not_load_item": "Kon item niet laden", - "none": "Geen", - "download": { - "download_season": "Download Seizoen", - "download_series": "Download Serie", - "download_episode": "Download Aflevering", - "download_movie": "Download Film", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Geoptimaliseerde server gebruiken", - "using_default_method": "Standaard methode gebruiken" - } - }, - "live_tv": { - "next": "Volgende ", - "previous": "Vorige", - "live_tv": "Live TV", - "coming_soon": "Binnenkort beschikbaar", - "on_now": "Nu op", - "shows": "Shows", - "movies": "Films", - "sports": "Sport", - "for_kids": "Voor kinderen", - "news": "Nieuws" - }, - "jellyseerr": { - "confirm": "Bevestig", - "cancel": "Annuleer", - "yes": "Ja", - "whats_wrong": "Wat is er mis?", - "issue_type": "Type probleem", - "select_an_issue": "Selecteer een probleem", - "types": "Types", - "describe_the_issue": "(optioneel) beschrijf het probleem...", - "submit_button": "Verzenden", - "report_issue_button": "Meld een probleem", - "request_button": "Aanvragen", - "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", - "failed_to_login": "Kon niet aanmelden", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Originele titel", - "series_type": "Serietype", - "release_dates": "Verschijningsdatums", - "first_air_date": "Eerste uitzenddatum", - "next_air_date": "Volgende uitzenddatum", - "revenue": "Inkomsten", - "budget": "Budget", - "original_language": "Originele taal", - "production_country": "Land van productie", - "studios": "Studio", - "network": "Netwerk", - "currently_streaming_on": "Momenteel te streamen op", - "advanced": "Geavanceerd", - "request_as": "Vraag aan als", - "tags": "Labels", - "quality_profile": "Kwaliteitsprofiel", - "root_folder": "Hoofdmap", - "season_all": "Season (all)", - "season_number": "Seizoen {{season_number}}", - "number_episodes": "{{episode_number}} Afleveringen", - "born": "Geboren", - "appearances": "Verschijningen", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", - "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", - "issue_submitted": "Probleem ingediend!", - "requested_item": "{{item}} aangevraagd!", - "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", - "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" - } - }, - "tabs": { - "home": "Thuis", - "search": "Zoeken", - "library": "Bibliotheek", - "custom_links": "Aangepaste links", - "favorites": "Favorieten" - } + "login": { + "username_required": "Gebruikersnaam is verplicht", + "error_title": "Fout", + "login_title": "Aanmelden", + "login_to_title": "Aanmelden bij", + "username_placeholder": "Gebruikersnaam", + "password_placeholder": "Wachtwoord", + "login_button": "Aanmelden", + "quick_connect": "Snel Verbinden", + "enter_code_to_login": "Vul code {{code}} in om aan te melden", + "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", + "got_it": "Begrepen", + "connection_failed": "Verbinding mislukt", + "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", + "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", + "change_server": "Server wijzigen", + "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", + "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", + "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", + "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", + "there_is_a_server_error": "Er is een serverfout", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" + }, + "server": { + "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", + "server_url_placeholder": "http(s)://je-server.com", + "connect_button": "Verbinden", + "previous_servers": "vorige servers", + "clear_button": "Wissen", + "search_for_local_servers": "Zoek naar lokale servers", + "searching": "Zoeken...", + "servers": "Servers" + }, + "home": { + "no_internet": "Geen Internet", + "no_items": "Geen items", + "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", + "go_to_downloads": "Ga naar downloads", + "oops": "Oeps!", + "error_message": "Er ging iets fout\nGelieve af en aan te melden.", + "continue_watching": "Verder Kijken", + "next_up": "Volgende", + "recently_added_in": "Recent toegevoegd in {{libraryName}}", + "suggested_movies": "Voorgestelde films", + "suggested_episodes": "Voorgestelde Afleveringen", + "intro": { + "welcome_to_streamyfin": "Welkom bij Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", + "features_title": "Functies", + "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", + "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", + "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", + "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", + "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", + "done_button": "Gedaan", + "go_to_settings_button": "Ga naar instellingen", + "read_more": "Lees meer" + }, + "settings": { + "settings_title": "Instellingen", + "log_out_button": "Afmelden", + "user_info": { + "user_info_title": "Gebruiker Info", + "user": "Gebruiker", + "server": "Server", + "token": "Token", + "app_version": "App Versie" + }, + "quick_connect": { + "quick_connect_title": "Snel Verbinden", + "authorize_button": "Snel Verbinden toestaan", + "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", + "success": "Succes", + "quick_connect_autorized": "Snel Verbinden toegestaan", + "error": "Fout", + "invalid_code": "Ongeldige code", + "authorize": "Toestaan" + }, + "media_controls": { + "media_controls_title": "Media Bedieningen", + "forward_skip_length": "Duur voorwaarts overslaan", + "rewind_length": "Duur terugspoelen", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Gebruik Audio Track Van Vorig Item", + "audio_language": "Audio taal", + "audio_hint": "Kies een standaard audio taal.", + "none": "Geen", + "language": "Taal" + }, + "subtitles": { + "subtitle_title": "Ondertitels", + "subtitle_language": "Ondertitel taal", + "subtitle_mode": "Ondertitelmodus", + "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", + "subtitle_size": "Ondertitel Grootte", + "subtitle_hint": "Stel ondertitel voorkeuren in.", + "none": "Geen", + "language": "Taal", + "loading": "Laden", + "modes": { + "Default": "Standaard", + "Smart": "Slim", + "Always": "Altijd", + "None": "Geen", + "OnlyForced": "Alleen Geforceerd" + } + }, + "other": { + "other_title": "Andere", + "follow_device_orientation": "Automatisch draaien", + "video_orientation": "Video oriëntatie", + "orientation": "Oriëntatie", + "orientations": { + "DEFAULT": "Standaard", + "ALL": "Alle", + "PORTRAIT": "Portret", + "PORTRAIT_UP": "Portret Omhoog", + "PORTRAIT_DOWN": "Portret Omlaag", + "LANDSCAPE": "Landschap", + "LANDSCAPE_LEFT": "Landschap Links", + "LANDSCAPE_RIGHT": "Landschap Rechts", + "OTHER": "Andere", + "UNKNOWN": "Onbekend" + }, + "safe_area_in_controls": "Veilig gebied in bedieningen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Aangepaste menulinks tonen", + "hide_libraries": "Verberg Bibliotheken", + "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", + "disable_haptic_feedback": "Haptische feedback uitschakelen", + "default_quality": "Standaard kwaliteit" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download methode", + "remux_max_download": "Maximale Remux-download", + "auto_download": "Auto download", + "optimized_versions_server": "Geoptimaliseerde server versies", + "save_button": "Opslaan", + "optimized_server": "Geoptimaliseerde Server", + "optimized": "Geoptimaliseerd", + "default": "Standaard", + "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", + "server_url": "Server URL", + "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Wachtwoord", + "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", + "save_button": "Opslaan", + "clear_button": "Wissen", + "login_button": "Aanmelden", + "total_media_requests": "Totaal aantal mediaverzoeken", + "movie_quota_limit": "Limiet filmquota", + "movie_quota_days": "Filmquota dagen", + "tv_quota_limit": "Limiet serie quota", + "tv_quota_days": "Serie Quota dagen", + "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", + "unlimited": "Ongelimiteerd", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Search inschakelen ", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort", + "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_marlin": "Lees meer over Marlin.", + "save_button": "Opslaan", + "toasts": { + "saved": "Opgeslagen" + } + } + }, + "storage": { + "storage_title": "Opslag", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Toestel {{availableSpace}}%", + "size_used": "{{used}} van {{total}} gebruikt", + "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" + }, + "intro": { + "show_intro": "Toon intro", + "reset_intro": "intro opnieuw instellen" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Geen logs beschikbaar", + "delete_all_logs": "Verwijder alle logs" + }, + "languages": { + "title": "Talen", + "app_language": "App taal", + "app_language_description": "Selecteer een taal voor de app.", + "system": "Systeem" + }, + "toasts": { + "error_deleting_files": "Fout bij het verwijderen van bestanden", + "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", + "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", + "connected": "Verbonden", + "could_not_connect": "Kon niet verbinden", + "invalid_url": "Ongeldige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "Series", + "movies": "Films", + "queue": "Wachtrij", + "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", + "no_items_in_queue": "Geen items in wachtrij", + "no_downloaded_items": "Geen gedownloade items", + "delete_all_movies_button": "Verwijder alle films", + "delete_all_tvseries_button": "Verwijder alle Series", + "delete_all_button": "Verwijder alles", + "active_download": "Actieve download", + "no_active_downloads": "Geen actieve downloads", + "active_downloads": "Actieve downloads", + "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", + "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", + "back": "Terug", + "delete": "Verwijder", + "something_went_wrong": "Er ging iets mis", + "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", + "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", + "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", + "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", + "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", + "download_cancelled": "Download geannuleerd", + "could_not_cancel_download": "Kon de download niet annuleren", + "download_completed": "Download afgerond", + "download_started_for": "Download gestart voor {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", + "download_stated_for_item": "Download gestart voor {{item}}", + "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", + "download_completed_for_item": "Download afgerond voor {{item}}", + "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", + "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", + "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", + "no_response_received_from_server": "Geen antwoord gekregen van de server", + "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", + "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", + "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", + "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", + "go_to_downloads": "Ga naar downloads" + } + } + }, + "search": { + "search_here": "Zoek hier...", + "search": "Zoek...", + "x_items": "{{count}} items", + "library": "Bibliotheek", + "discover": "Ontdek", + "no_results": "Geen resultaten", + "no_results_found_for": "Geen resultaten gevonden voor", + "movies": "Films", + "series": "Series", + "episodes": "Afleveringen", + "collections": "Collecties", + "actors": "Acteurs", + "request_movies": "Vraag films aan", + "request_series": "Vraag series aan", + "recently_added": "Recent Toegevoegd", + "recent_requests": "Recent Aangevraagd", + "plex_watchlist": "Plex Kijklijst", + "trending": "Trending", + "popular_movies": "Populaire films", + "movie_genres": "Film Genres", + "upcoming_movies": "Aankomende films", + "studios": "Studios", + "popular_tv": "Populaire TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Aankomende TV", + "networks": "Netwerken", + "tmdb_movie_keyword": "TMDB Film Trefwoord", + "tmdb_movie_genre": "TMDB Filmgenres", + "tmdb_tv_keyword": "TMDB TV Trefwoord", + "tmdb_tv_genre": "TMDB TV-Genres", + "tmdb_search": "TMDB Zoeken", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netwerk", + "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", + "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" + }, + "library": { + "no_items_found": "Geen items gevonden", + "no_results": "Geen resultaten", + "no_libraries_found": "Geen bibliotheken gevonden", + "item_types": { + "movies": "Films", + "series": "Series", + "boxsets": "Boxsets", + "items": "items" + }, + "options": { + "display": "Weergave", + "row": "Rij", + "list": "Lijst", + "image_style": "Stijl van afbeelding", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Toon titels", + "show_stats": "Toon statistieken" + }, + "filters": { + "genres": "Genres", + "years": "Jaren", + "sort_by": "Sorteren op", + "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", + "tags": "Labels" + } + }, + "favorites": { + "series": "Series", + "movies": "Films", + "episodes": "Afleveringen", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Afspeellijsten", + "noDataTitle": "Nog geen favorieten", + "noData": "Markeer items als favoriet om ze hier te laten verschijnen voor snelle toegang." + }, + "custom_links": { + "no_links": "Geen links" + }, + "player": { + "error": "Fout", + "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", + "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", + "client_error": "Fout van de client", + "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", + "message_from_server": "Bericht van de server", + "video_has_finished_playing": "Video is gedaan met spelen!", + "no_video_source": "Geen videobron...", + "next_episode": "Volgende Aflevering", + "refresh_tracks": "Tracks verversen", + "subtitle_tracks": "Ondertitel Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Afspeelstatus:", + "no_data_available": "Geen data beschikbaar", + "index": "Index:" + }, + "item_card": { + "next_up": "Volgende", + "no_items_to_display": "Geen items om te tonen", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seizoenen", + "season": "Seizoen", + "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", + "overview": "Overzicht", + "more_with": "Meer met {{name}}", + "similar_items": "Gelijkaardige items", + "no_similar_items_found": "Geen gelijkaardige items gevonden", + "video": "Video", + "more_details": "Meer details", + "quality": "Kwaliteit", + "audio": "Audio", + "subtitles": "Ondertitel", + "show_more": "Toon meer", + "show_less": "Toon minder", + "appeared_in": "Verschenen in", + "could_not_load_item": "Kon item niet laden", + "none": "Geen", + "download": { + "download_season": "Download Seizoen", + "download_series": "Download Serie", + "download_episode": "Download Aflevering", + "download_movie": "Download Film", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Geoptimaliseerde server gebruiken", + "using_default_method": "Standaard methode gebruiken" + } + }, + "live_tv": { + "next": "Volgende ", + "previous": "Vorige", + "live_tv": "Live TV", + "coming_soon": "Binnenkort beschikbaar", + "on_now": "Nu op", + "shows": "Shows", + "movies": "Films", + "sports": "Sport", + "for_kids": "Voor kinderen", + "news": "Nieuws" + }, + "jellyseerr": { + "confirm": "Bevestig", + "cancel": "Annuleer", + "yes": "Ja", + "whats_wrong": "Wat is er mis?", + "issue_type": "Type probleem", + "select_an_issue": "Selecteer een probleem", + "types": "Types", + "describe_the_issue": "(optioneel) beschrijf het probleem...", + "submit_button": "Verzenden", + "report_issue_button": "Meld een probleem", + "request_button": "Aanvragen", + "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", + "failed_to_login": "Kon niet aanmelden", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Originele titel", + "series_type": "Serietype", + "release_dates": "Verschijningsdatums", + "first_air_date": "Eerste uitzenddatum", + "next_air_date": "Volgende uitzenddatum", + "revenue": "Inkomsten", + "budget": "Budget", + "original_language": "Originele taal", + "production_country": "Land van productie", + "studios": "Studio", + "network": "Netwerk", + "currently_streaming_on": "Momenteel te streamen op", + "advanced": "Geavanceerd", + "request_as": "Vraag aan als", + "tags": "Labels", + "quality_profile": "Kwaliteitsprofiel", + "root_folder": "Hoofdmap", + "season_all": "Season (all)", + "season_number": "Seizoen {{season_number}}", + "number_episodes": "{{episode_number}} Afleveringen", + "born": "Geboren", + "appearances": "Verschijningen", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", + "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", + "issue_submitted": "Probleem ingediend!", + "requested_item": "{{item}} aangevraagd!", + "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", + "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" + } + }, + "tabs": { + "home": "Thuis", + "search": "Zoeken", + "library": "Bibliotheek", + "custom_links": "Aangepaste links", + "favorites": "Favorieten" + } } diff --git a/translations/sv.json b/translations/sv.json index c5a10124..65cc3aed 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -1,34 +1,34 @@ { - "login": { - "username_required": "Användarnamn krävs", - "error_title": "Fel", - "login_title": "Logga in", - "username_placeholder": "Användarnamn", - "password_placeholder": "Lösenord", - "login_button": "Logga in" - }, - "server": { - "server_url_placeholder": "Server URL", - "connect_button": "Anslut" - }, - "home": { - "home": "Hem", - "no_internet": "Ingen Internet", - "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", - "go_to_downloads": "Gå till nedladdningar", - "oops": "Hoppsan!", - "error_message": "Något gick fel.\nLogga ut och in igen.", - "continue_watching": "Fortsätt titta", - "next_up": "Nästa upp", - "recently_added_in": "Nyligen tillagt i {{libraryName}}" - }, - "favorites": { - "noDataTitle": "Inga favoriter än", - "noData": "Markera objekt som favoriter för att se dem visas här för snabb åtkomst." - }, - "tabs": { - "home": "Hem", - "search": "Sök", - "library": "Bibliotek" - } + "login": { + "username_required": "Användarnamn krävs", + "error_title": "Fel", + "login_title": "Logga in", + "username_placeholder": "Användarnamn", + "password_placeholder": "Lösenord", + "login_button": "Logga in" + }, + "server": { + "server_url_placeholder": "Server URL", + "connect_button": "Anslut" + }, + "home": { + "home": "Hem", + "no_internet": "Ingen Internet", + "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", + "go_to_downloads": "Gå till nedladdningar", + "oops": "Hoppsan!", + "error_message": "Något gick fel.\nLogga ut och in igen.", + "continue_watching": "Fortsätt titta", + "next_up": "Nästa upp", + "recently_added_in": "Nyligen tillagt i {{libraryName}}" + }, + "favorites": { + "noDataTitle": "Inga favoriter än", + "noData": "Markera objekt som favoriter för att se dem visas här för snabb åtkomst." + }, + "tabs": { + "home": "Hem", + "search": "Sök", + "library": "Bibliotek" + } } diff --git a/translations/tr.json b/translations/tr.json index 7886423c..6d1a6714 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "Kullanıcı adı gereklidir", - "error_title": "Hata", - "login_title": "Giriş yap", - "login_to_title": " 'e giriş yap", - "username_placeholder": "Kullanıcı adı", - "password_placeholder": "Şifre", - "login_button": "Giriş yap", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", - "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", - "got_it": "Anlaşıldı", - "connection_failed": "Bağlantı başarısız", - "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", - "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", - "change_server": "Sunucuyu değiştir", - "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", - "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", - "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", - "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", - "there_is_a_server_error": "Sunucu hatası var", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" - }, - "server": { - "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", - "server_url_placeholder": "http(s)://sunucunuz.com", - "connect_button": "Bağlan", - "previous_servers": "Önceki sunucular", - "clear_button": "Temizle", - "search_for_local_servers": "Yerel sunucuları ara", - "searching": "Aranıyor...", - "servers": "Sunucular" - }, - "home": { - "no_internet": "İnternet Yok", - "no_items": "Öge Yok", - "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", - "go_to_downloads": "İndirmelere Git", - "oops": "Hups!", - "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", - "continue_watching": "İzlemeye Devam Et", - "next_up": "Sonraki", - "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", - "suggested_movies": "Önerilen Filmler", - "suggested_episodes": "Önerilen Bölümler", - "intro": { - "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", - "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", - "features_title": "Özellikler", - "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", - "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", - "downloads_feature_title": "İndirmeler", - "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", - "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", - "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", - "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", - "done_button": "Tamam", - "go_to_settings_button": "Ayrıntılara Git", - "read_more": "Daha fazla oku" - }, - "settings": { - "settings_title": "Ayarlar", - "log_out_button": "Çıkış Yap", - "user_info": { - "user_info_title": "Kullanıcı Bilgisi", - "user": "Kullanıcı", - "server": "Sunucu", - "token": "Token", - "app_version": "Uygulama Sürümü" - }, - "quick_connect": { - "quick_connect_title": "Hızlı Bağlantı", - "authorize_button": "Hızlı Bağlantıyı Yetkilendir", - "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", - "success": "Başarılı", - "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", - "error": "Hata", - "invalid_code": "Geçersiz kod", - "authorize": "Yetkilendir" - }, - "media_controls": { - "media_controls_title": "Medya Kontrolleri", - "forward_skip_length": "İleri Sarma Uzunluğu", - "rewind_length": "Geri Sarma Uzunluğu", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Ses", - "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", - "audio_language": "Ses Dili", - "audio_hint": "Varsayılan ses dilini seçin.", - "none": "Yok", - "language": "Dil" - }, - "subtitles": { - "subtitle_title": "Altyazılar", - "subtitle_language": "Altyazı Dili", - "subtitle_mode": "Altyazı Modu", - "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", - "subtitle_size": "Altyazı Boyutu", - "subtitle_hint": "Altyazı tercihini yapılandırın.", - "none": "Yok", - "language": "Dil", - "loading": "Yükleniyor", - "modes": { - "Default": "Varsayılan", - "Smart": "Akıllı", - "Always": "Her Zaman", - "None": "Yok", - "OnlyForced": "Sadece Zorunlu" - } - }, - "other": { - "other_title": "Diğer", - "follow_device_orientation": "Otomatik Döndürme", - "video_orientation": "Video Yönü", - "orientation": "Yön", - "orientations": { - "DEFAULT": "Varsayılan", - "ALL": "Tümü", - "PORTRAIT": "Dikey", - "PORTRAIT_UP": "Dikey Yukarı", - "PORTRAIT_DOWN": "Dikey Aşağı", - "LANDSCAPE": "Yatay", - "LANDSCAPE_LEFT": "Yatay Sol", - "LANDSCAPE_RIGHT": "Yatay Sağ", - "OTHER": "Diğer", - "UNKNOWN": "Bilinmeyen" - }, - "safe_area_in_controls": "Kontrollerde Güvenli Alan", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", - "hide_libraries": "Kütüphaneleri Gizle", - "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", - "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" - }, - "downloads": { - "downloads_title": "İndirmeler", - "download_method": "İndirme Yöntemi", - "remux_max_download": "Remux max indirme", - "auto_download": "Otomatik İndirme", - "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", - "save_button": "Kaydet", - "optimized_server": "Optimize Sunucu", - "optimized": "Optimize", - "default": "Varsayılan", - "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Eklentiler", - "jellyseerr": { - "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", - "server_url": "Sunucu URL'si", - "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Şifre", - "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", - "save_button": "Kaydet", - "clear_button": "Temizle", - "login_button": "Giriş Yap", - "total_media_requests": "Toplam medya istekleri", - "movie_quota_limit": "Film kota limiti", - "movie_quota_days": "Film kota günleri", - "tv_quota_limit": "TV kota limiti", - "tv_quota_days": "TV kota günleri", - "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", - "unlimited": "Sınırsız", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Aramasını Etkinleştir ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_marlin": "Marlin hakkında daha fazla oku.", - "save_button": "Kaydet", - "toasts": { - "saved": "Kaydedildi" - } - } - }, - "storage": { - "storage_title": "Depolama", - "app_usage": "Uygulama {{usedSpace}}%", - "device_usage": "Cihaz {{availableSpace}}%", - "size_used": "{{used}} / {{total}} kullanıldı", - "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" - }, - "intro": { - "show_intro": "Tanıtımı Göster", - "reset_intro": "Tanıtımı Sıfırla" - }, - "logs": { - "logs_title": "Günlükler", - "no_logs_available": "Günlükler mevcut değil", - "delete_all_logs": "Tüm günlükleri sil" - }, - "languages": { - "title": "Diller", - "app_language": "Uygulama dili", - "app_language_description": "Uygulama dilini seçin.", - "system": "Sistem" - }, - "toasts": { - "error_deleting_files": "Dosyalar silinirken hata oluştu", - "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", - "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", - "connected": "Bağlandı", - "could_not_connect": "Bağlanılamadı", - "invalid_url": "Geçersiz URL" - } - }, - "downloads": { - "downloads_title": "İndirilenler", - "tvseries": "Diziler", - "movies": "Filmler", - "queue": "Sıra", - "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", - "no_items_in_queue": "Sırada öğe yok", - "no_downloaded_items": "İndirilen öğe yok", - "delete_all_movies_button": "Tüm Filmleri Sil", - "delete_all_tvseries_button": "Tüm Dizileri Sil", - "delete_all_button": "Tümünü Sil", - "active_download": "Aktif indirme", - "no_active_downloads": "Aktif indirme yok", - "active_downloads": "Aktif indirmeler", - "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", - "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", - "back": "Geri", - "delete": "Sil", - "something_went_wrong": "Bir şeyler ters gitti", - "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", - "eta": "Tahmini Süre {{eta}}", - "methods": "Yöntemler", - "toasts": { - "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", - "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", - "failed_to_delete_all_movies": "Filmler silinemedi", - "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", - "failed_to_delete_all_tvseries": "Diziler silinemedi", - "download_cancelled": "İndirme iptal edildi", - "could_not_cancel_download": "İndirme iptal edilemedi", - "download_completed": "İndirme tamamlandı", - "download_started_for": "{{item}} için indirme başlatıldı", - "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", - "download_stated_for_item": "{{item}} için indirme başlatıldı", - "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", - "download_completed_for_item": "{{item}} için indirme tamamlandı", - "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", - "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", - "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", - "no_response_received_from_server": "Sunucudan yanıt alınamadı", - "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", - "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", - "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", - "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", - "go_to_downloads": "İndirmelere git" - } - } - }, - "search": { - "search_here": "Burada ara...", - "search": "Ara...", - "x_items": "{{count}} öge(ler)", - "library": "Kütüphane", - "discover": "Keşfet", - "no_results": "Sonuç bulunamadı", - "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", - "movies": "Filmler", - "series": "Diziler", - "episodes": "Bölümler", - "collections": "Koleksiyonlar", - "actors": "Oyuncular", - "request_movies": "Film Talep Et", - "request_series": "Dizi Talep Et", - "recently_added": "Son Eklenenler", - "recent_requests": "Son Talepler", - "plex_watchlist": "Plex İzleme Listesi", - "trending": "Şu An Popüler", - "popular_movies": "Popüler Filmler", - "movie_genres": "Film Türleri", - "upcoming_movies": "Yaklaşan Filmler", - "studios": "Stüdyolar", - "popular_tv": "Popüler Diziler", - "tv_genres": "Dizi Türleri", - "upcoming_tv": "Yaklaşan Diziler", - "networks": "Ağlar", - "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", - "tmdb_movie_genre": "TMDB Film Türü", - "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", - "tmdb_tv_genre": "TMDB Dizi Türü", - "tmdb_search": "TMDB Arama", - "tmdb_studio": "TMDB Stüdyo", - "tmdb_network": "TMDB Ağ", - "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", - "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" - }, - "library": { - "no_items_found": "Öğe bulunamadı", - "no_results": "Sonuç bulunamadı", - "no_libraries_found": "Kütüphane bulunamadı", - "item_types": { - "movies": "filmler", - "series": "diziler", - "boxsets": "koleksiyonlar", - "items": "ögeler" - }, - "options": { - "display": "Görüntüleme", - "row": "Satır", - "list": "Liste", - "image_style": "Görsel stili", - "poster": "Poster", - "cover": "Kapak", - "show_titles": "Başlıkları göster", - "show_stats": "İstatistikleri göster" - }, - "filters": { - "genres": "Türler", - "years": "Yıllar", - "sort_by": "Sırala", - "sort_order": "Sıralama düzeni", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiketler" - } - }, - "favorites": { - "series": "Diziler", - "movies": "Filmler", - "episodes": "Bölümler", - "videos": "Videolar", - "boxsets": "Koleksiyonlar", - "playlists": "Çalma listeleri", - "noDataTitle": "Henüz favori yok", - "noData": "Hızlı erişim için öğeleri favori olarak işaretleyin ve burada görünmelerini sağlayın." - }, - "custom_links": { - "no_links": "Bağlantı yok" - }, - "player": { - "error": "Hata", - "failed_to_get_stream_url": "Yayın URL'si alınamadı", - "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", - "client_error": "İstemci hatası", - "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", - "message_from_server": "Sunucudan mesaj: {{message}}", - "video_has_finished_playing": "Video oynatıldı!", - "no_video_source": "Video kaynağı yok...", - "next_episode": "Sonraki bölüm", - "refresh_tracks": "Parçaları yenile", - "subtitle_tracks": "Altyazı Parçaları:", - "audio_tracks": "Ses Parçaları:", - "playback_state": "Oynatma Durumu:", - "no_data_available": "Veri bulunamadı", - "index": "İndeks:" - }, - "item_card": { - "next_up": "Sıradaki", - "no_items_to_display": "Görüntülenecek öğe yok", - "cast_and_crew": "Oyuncular & Ekip", - "series": "Dizi", - "seasons": "Sezonlar", - "season": "Sezon", - "no_episodes_for_this_season": "Bu sezona ait bölüm yok", - "overview": "Özet", - "more_with": "Daha fazla {{name}}", - "similar_items": "Benzer ögeler", - "no_similar_items_found": "Benzer öge bulunamadı", - "video": "Video", - "more_details": "Daha fazla detay", - "quality": "Kalite", - "audio": "Ses", - "subtitles": "Altyazı", - "show_more": "Daha fazla göster", - "show_less": "Daha az göster", - "appeared_in": "Şurada yer aldı", - "could_not_load_item": "Öge yüklenemedi", - "none": "Hiçbiri", - "download": { - "download_season": "Sezonu indir", - "download_series": "Diziyi indir", - "download_episode": "Bölümü indir", - "download_movie": "Filmi indir", - "download_x_item": "{{item_count}} tane ögeyi indir", - "download_button": "İndir", - "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", - "using_default_method": "Varsayılan yöntem kullanılıyor" - } - }, - "live_tv": { - "next": "Sonraki", - "previous": "Önceki", - "live_tv": "Canlı TV", - "coming_soon": "Yakında", - "on_now": "Şu anda yayında", - "shows": "Programlar", - "movies": "Filmler", - "sports": "Spor", - "for_kids": "Çocuklar İçin", - "news": "Haberler" - }, - "jellyseerr": { - "confirm": "Onayla", - "cancel": "İptal", - "yes": "Evet", - "whats_wrong": "Problem nedir?", - "issue_type": "Sorun türü", - "select_an_issue": "Bir sorun seçin", - "types": "Türler", - "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", - "submit_button": "Gönder", - "report_issue_button": "Sorunu bildir", - "request_button": "Talep et", - "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", - "failed_to_login": "Giriş yapılamadı", - "cast": "Oyuncular", - "details": "Detaylar", - "status": "Durum", - "original_title": "Orijinal Başlık", - "series_type": "Dizi Türü", - "release_dates": "Yayın Tarihleri", - "first_air_date": "İlk Yayın Tarihi", - "next_air_date": "Sonraki Yayın Tarihi", - "revenue": "Gelir", - "budget": "Bütçe", - "original_language": "Orijinal Dil", - "production_country": "Yapım Ülkesi", - "studios": "Stüdyolar", - "network": "Ağ", - "currently_streaming_on": "Şu anda yayınlanıyor", - "advanced": "Gelişmiş", - "request_as": "Şu olarak iste", - "tags": "Etiketler", - "quality_profile": "Kalite Profili", - "root_folder": "Kök Klasör", - "season_all": "Season (all)", - "season_number": "Sezon {{season_number}}", - "number_episodes": "Bölüm {{episode_number}}", - "born": "Doğum", - "appearances": "Görünmeler", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", - "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", - "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", - "issue_submitted": "Sorun gönderildi!", - "requested_item": "{{item}} talep edildi!", - "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", - "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" - } - }, - "tabs": { - "home": "Ana Sayfa", - "search": "Ara", - "library": "Kütüphane", - "custom_links": "Özel Bağlantılar", - "favorites": "Favoriler" - } + "login": { + "username_required": "Kullanıcı adı gereklidir", + "error_title": "Hata", + "login_title": "Giriş yap", + "login_to_title": " 'e giriş yap", + "username_placeholder": "Kullanıcı adı", + "password_placeholder": "Şifre", + "login_button": "Giriş yap", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", + "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", + "got_it": "Anlaşıldı", + "connection_failed": "Bağlantı başarısız", + "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", + "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", + "change_server": "Sunucuyu değiştir", + "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", + "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", + "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", + "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", + "there_is_a_server_error": "Sunucu hatası var", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" + }, + "server": { + "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", + "server_url_placeholder": "http(s)://sunucunuz.com", + "connect_button": "Bağlan", + "previous_servers": "Önceki sunucular", + "clear_button": "Temizle", + "search_for_local_servers": "Yerel sunucuları ara", + "searching": "Aranıyor...", + "servers": "Sunucular" + }, + "home": { + "no_internet": "İnternet Yok", + "no_items": "Öge Yok", + "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", + "go_to_downloads": "İndirmelere Git", + "oops": "Hups!", + "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", + "continue_watching": "İzlemeye Devam Et", + "next_up": "Sonraki", + "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", + "suggested_movies": "Önerilen Filmler", + "suggested_episodes": "Önerilen Bölümler", + "intro": { + "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", + "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", + "features_title": "Özellikler", + "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", + "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", + "downloads_feature_title": "İndirmeler", + "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", + "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", + "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", + "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", + "done_button": "Tamam", + "go_to_settings_button": "Ayrıntılara Git", + "read_more": "Daha fazla oku" + }, + "settings": { + "settings_title": "Ayarlar", + "log_out_button": "Çıkış Yap", + "user_info": { + "user_info_title": "Kullanıcı Bilgisi", + "user": "Kullanıcı", + "server": "Sunucu", + "token": "Token", + "app_version": "Uygulama Sürümü" + }, + "quick_connect": { + "quick_connect_title": "Hızlı Bağlantı", + "authorize_button": "Hızlı Bağlantıyı Yetkilendir", + "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", + "success": "Başarılı", + "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", + "error": "Hata", + "invalid_code": "Geçersiz kod", + "authorize": "Yetkilendir" + }, + "media_controls": { + "media_controls_title": "Medya Kontrolleri", + "forward_skip_length": "İleri Sarma Uzunluğu", + "rewind_length": "Geri Sarma Uzunluğu", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Ses", + "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", + "audio_language": "Ses Dili", + "audio_hint": "Varsayılan ses dilini seçin.", + "none": "Yok", + "language": "Dil" + }, + "subtitles": { + "subtitle_title": "Altyazılar", + "subtitle_language": "Altyazı Dili", + "subtitle_mode": "Altyazı Modu", + "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", + "subtitle_size": "Altyazı Boyutu", + "subtitle_hint": "Altyazı tercihini yapılandırın.", + "none": "Yok", + "language": "Dil", + "loading": "Yükleniyor", + "modes": { + "Default": "Varsayılan", + "Smart": "Akıllı", + "Always": "Her Zaman", + "None": "Yok", + "OnlyForced": "Sadece Zorunlu" + } + }, + "other": { + "other_title": "Diğer", + "follow_device_orientation": "Otomatik Döndürme", + "video_orientation": "Video Yönü", + "orientation": "Yön", + "orientations": { + "DEFAULT": "Varsayılan", + "ALL": "Tümü", + "PORTRAIT": "Dikey", + "PORTRAIT_UP": "Dikey Yukarı", + "PORTRAIT_DOWN": "Dikey Aşağı", + "LANDSCAPE": "Yatay", + "LANDSCAPE_LEFT": "Yatay Sol", + "LANDSCAPE_RIGHT": "Yatay Sağ", + "OTHER": "Diğer", + "UNKNOWN": "Bilinmeyen" + }, + "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", + "hide_libraries": "Kütüphaneleri Gizle", + "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", + "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" + }, + "downloads": { + "downloads_title": "İndirmeler", + "download_method": "İndirme Yöntemi", + "remux_max_download": "Remux max indirme", + "auto_download": "Otomatik İndirme", + "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", + "save_button": "Kaydet", + "optimized_server": "Optimize Sunucu", + "optimized": "Optimize", + "default": "Varsayılan", + "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Eklentiler", + "jellyseerr": { + "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", + "server_url": "Sunucu URL'si", + "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Şifre", + "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", + "save_button": "Kaydet", + "clear_button": "Temizle", + "login_button": "Giriş Yap", + "total_media_requests": "Toplam medya istekleri", + "movie_quota_limit": "Film kota limiti", + "movie_quota_days": "Film kota günleri", + "tv_quota_limit": "TV kota limiti", + "tv_quota_days": "TV kota günleri", + "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", + "unlimited": "Sınırsız", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Aramasını Etkinleştir ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_marlin": "Marlin hakkında daha fazla oku.", + "save_button": "Kaydet", + "toasts": { + "saved": "Kaydedildi" + } + } + }, + "storage": { + "storage_title": "Depolama", + "app_usage": "Uygulama {{usedSpace}}%", + "device_usage": "Cihaz {{availableSpace}}%", + "size_used": "{{used}} / {{total}} kullanıldı", + "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" + }, + "intro": { + "show_intro": "Tanıtımı Göster", + "reset_intro": "Tanıtımı Sıfırla" + }, + "logs": { + "logs_title": "Günlükler", + "no_logs_available": "Günlükler mevcut değil", + "delete_all_logs": "Tüm günlükleri sil" + }, + "languages": { + "title": "Diller", + "app_language": "Uygulama dili", + "app_language_description": "Uygulama dilini seçin.", + "system": "Sistem" + }, + "toasts": { + "error_deleting_files": "Dosyalar silinirken hata oluştu", + "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", + "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", + "connected": "Bağlandı", + "could_not_connect": "Bağlanılamadı", + "invalid_url": "Geçersiz URL" + } + }, + "downloads": { + "downloads_title": "İndirilenler", + "tvseries": "Diziler", + "movies": "Filmler", + "queue": "Sıra", + "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", + "no_items_in_queue": "Sırada öğe yok", + "no_downloaded_items": "İndirilen öğe yok", + "delete_all_movies_button": "Tüm Filmleri Sil", + "delete_all_tvseries_button": "Tüm Dizileri Sil", + "delete_all_button": "Tümünü Sil", + "active_download": "Aktif indirme", + "no_active_downloads": "Aktif indirme yok", + "active_downloads": "Aktif indirmeler", + "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", + "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", + "back": "Geri", + "delete": "Sil", + "something_went_wrong": "Bir şeyler ters gitti", + "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", + "eta": "Tahmini Süre {{eta}}", + "methods": "Yöntemler", + "toasts": { + "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", + "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", + "failed_to_delete_all_movies": "Filmler silinemedi", + "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", + "failed_to_delete_all_tvseries": "Diziler silinemedi", + "download_cancelled": "İndirme iptal edildi", + "could_not_cancel_download": "İndirme iptal edilemedi", + "download_completed": "İndirme tamamlandı", + "download_started_for": "{{item}} için indirme başlatıldı", + "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", + "download_stated_for_item": "{{item}} için indirme başlatıldı", + "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", + "download_completed_for_item": "{{item}} için indirme tamamlandı", + "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", + "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", + "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", + "no_response_received_from_server": "Sunucudan yanıt alınamadı", + "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", + "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", + "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", + "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", + "go_to_downloads": "İndirmelere git" + } + } + }, + "search": { + "search_here": "Burada ara...", + "search": "Ara...", + "x_items": "{{count}} öge(ler)", + "library": "Kütüphane", + "discover": "Keşfet", + "no_results": "Sonuç bulunamadı", + "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", + "movies": "Filmler", + "series": "Diziler", + "episodes": "Bölümler", + "collections": "Koleksiyonlar", + "actors": "Oyuncular", + "request_movies": "Film Talep Et", + "request_series": "Dizi Talep Et", + "recently_added": "Son Eklenenler", + "recent_requests": "Son Talepler", + "plex_watchlist": "Plex İzleme Listesi", + "trending": "Şu An Popüler", + "popular_movies": "Popüler Filmler", + "movie_genres": "Film Türleri", + "upcoming_movies": "Yaklaşan Filmler", + "studios": "Stüdyolar", + "popular_tv": "Popüler Diziler", + "tv_genres": "Dizi Türleri", + "upcoming_tv": "Yaklaşan Diziler", + "networks": "Ağlar", + "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", + "tmdb_movie_genre": "TMDB Film Türü", + "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", + "tmdb_tv_genre": "TMDB Dizi Türü", + "tmdb_search": "TMDB Arama", + "tmdb_studio": "TMDB Stüdyo", + "tmdb_network": "TMDB Ağ", + "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", + "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" + }, + "library": { + "no_items_found": "Öğe bulunamadı", + "no_results": "Sonuç bulunamadı", + "no_libraries_found": "Kütüphane bulunamadı", + "item_types": { + "movies": "filmler", + "series": "diziler", + "boxsets": "koleksiyonlar", + "items": "ögeler" + }, + "options": { + "display": "Görüntüleme", + "row": "Satır", + "list": "Liste", + "image_style": "Görsel stili", + "poster": "Poster", + "cover": "Kapak", + "show_titles": "Başlıkları göster", + "show_stats": "İstatistikleri göster" + }, + "filters": { + "genres": "Türler", + "years": "Yıllar", + "sort_by": "Sırala", + "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiketler" + } + }, + "favorites": { + "series": "Diziler", + "movies": "Filmler", + "episodes": "Bölümler", + "videos": "Videolar", + "boxsets": "Koleksiyonlar", + "playlists": "Çalma listeleri", + "noDataTitle": "Henüz favori yok", + "noData": "Hızlı erişim için öğeleri favori olarak işaretleyin ve burada görünmelerini sağlayın." + }, + "custom_links": { + "no_links": "Bağlantı yok" + }, + "player": { + "error": "Hata", + "failed_to_get_stream_url": "Yayın URL'si alınamadı", + "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", + "client_error": "İstemci hatası", + "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", + "message_from_server": "Sunucudan mesaj: {{message}}", + "video_has_finished_playing": "Video oynatıldı!", + "no_video_source": "Video kaynağı yok...", + "next_episode": "Sonraki bölüm", + "refresh_tracks": "Parçaları yenile", + "subtitle_tracks": "Altyazı Parçaları:", + "audio_tracks": "Ses Parçaları:", + "playback_state": "Oynatma Durumu:", + "no_data_available": "Veri bulunamadı", + "index": "İndeks:" + }, + "item_card": { + "next_up": "Sıradaki", + "no_items_to_display": "Görüntülenecek öğe yok", + "cast_and_crew": "Oyuncular & Ekip", + "series": "Dizi", + "seasons": "Sezonlar", + "season": "Sezon", + "no_episodes_for_this_season": "Bu sezona ait bölüm yok", + "overview": "Özet", + "more_with": "Daha fazla {{name}}", + "similar_items": "Benzer ögeler", + "no_similar_items_found": "Benzer öge bulunamadı", + "video": "Video", + "more_details": "Daha fazla detay", + "quality": "Kalite", + "audio": "Ses", + "subtitles": "Altyazı", + "show_more": "Daha fazla göster", + "show_less": "Daha az göster", + "appeared_in": "Şurada yer aldı", + "could_not_load_item": "Öge yüklenemedi", + "none": "Hiçbiri", + "download": { + "download_season": "Sezonu indir", + "download_series": "Diziyi indir", + "download_episode": "Bölümü indir", + "download_movie": "Filmi indir", + "download_x_item": "{{item_count}} tane ögeyi indir", + "download_button": "İndir", + "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", + "using_default_method": "Varsayılan yöntem kullanılıyor" + } + }, + "live_tv": { + "next": "Sonraki", + "previous": "Önceki", + "live_tv": "Canlı TV", + "coming_soon": "Yakında", + "on_now": "Şu anda yayında", + "shows": "Programlar", + "movies": "Filmler", + "sports": "Spor", + "for_kids": "Çocuklar İçin", + "news": "Haberler" + }, + "jellyseerr": { + "confirm": "Onayla", + "cancel": "İptal", + "yes": "Evet", + "whats_wrong": "Problem nedir?", + "issue_type": "Sorun türü", + "select_an_issue": "Bir sorun seçin", + "types": "Türler", + "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", + "submit_button": "Gönder", + "report_issue_button": "Sorunu bildir", + "request_button": "Talep et", + "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", + "failed_to_login": "Giriş yapılamadı", + "cast": "Oyuncular", + "details": "Detaylar", + "status": "Durum", + "original_title": "Orijinal Başlık", + "series_type": "Dizi Türü", + "release_dates": "Yayın Tarihleri", + "first_air_date": "İlk Yayın Tarihi", + "next_air_date": "Sonraki Yayın Tarihi", + "revenue": "Gelir", + "budget": "Bütçe", + "original_language": "Orijinal Dil", + "production_country": "Yapım Ülkesi", + "studios": "Stüdyolar", + "network": "Ağ", + "currently_streaming_on": "Şu anda yayınlanıyor", + "advanced": "Gelişmiş", + "request_as": "Şu olarak iste", + "tags": "Etiketler", + "quality_profile": "Kalite Profili", + "root_folder": "Kök Klasör", + "season_all": "Season (all)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "Bölüm {{episode_number}}", + "born": "Doğum", + "appearances": "Görünmeler", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", + "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", + "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", + "issue_submitted": "Sorun gönderildi!", + "requested_item": "{{item}} talep edildi!", + "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", + "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" + } + }, + "tabs": { + "home": "Ana Sayfa", + "search": "Ara", + "library": "Kütüphane", + "custom_links": "Özel Bağlantılar", + "favorites": "Favoriler" + } } diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 7d8e09db..e9e0391a 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "需要用户名", - "error_title": "错误", - "login_title": "登录", - "login_to_title": "登录至", - "username_placeholder": "用户名", - "password_placeholder": "密码", - "login_button": "登录", - "quick_connect": "快速连接", - "enter_code_to_login": "输入代码 {{code}} 以登录", - "failed_to_initiate_quick_connect": "无法启动快速连接", - "got_it": "了解", - "connection_failed": "连接失败", - "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", - "an_unexpected_error_occured": "发生意外错误", - "change_server": "更改服务器", - "invalid_username_or_password": "无效的用户名或密码", - "user_does_not_have_permission_to_log_in": "用户没有登录权限", - "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", - "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", - "there_is_a_server_error": "服务器出错", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "连接", - "previous_servers": "上一个服务器", - "clear_button": "清除", - "search_for_local_servers": "搜索本地服务器", - "searching": "搜索中...", - "servers": "服务器" - }, - "home": { - "no_internet": "无网络", - "no_items": "无项目", - "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", - "go_to_downloads": "前往下载", - "oops": "哎呀!", - "error_message": "出错了。\n请注销重新登录。", - "continue_watching": "继续观看", - "next_up": "下一个", - "recently_added_in": "最近添加于 {{libraryName}}", - "suggested_movies": "推荐电影", - "suggested_episodes": "推荐剧集", - "intro": { - "welcome_to_streamyfin": "欢迎来到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", - "features_title": "功能", - "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", - "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", - "downloads_feature_title": "下载", - "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", - "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", - "centralised_settings_plugin_title": "统一设置插件", - "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", - "done_button": "完成", - "go_to_settings_button": "前往设置", - "read_more": "了解更多" - }, - "settings": { - "settings_title": "设置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用户信息", - "user": "用户", - "server": "服务器", - "token": "密钥", - "app_version": "应用版本" - }, - "quick_connect": { - "quick_connect_title": "快速连接", - "authorize_button": "授权快速连接", - "enter_the_quick_connect_code": "输入快速连接代码...", - "success": "成功", - "quick_connect_autorized": "快速连接已授权", - "error": "错误", - "invalid_code": "无效代码", - "authorize": "授权" - }, - "media_controls": { - "media_controls_title": "媒体控制", - "forward_skip_length": "快进时长", - "rewind_length": "快退时长", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音频", - "set_audio_track": "从上一个项目设置音轨", - "audio_language": "音频语言", - "audio_hint": "选择默认音频语言。", - "none": "无", - "language": "语言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕语言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "从上一个项目设置字幕", - "subtitle_size": "字幕大小", - "subtitle_hint": "设置字幕偏好。", - "none": "无", - "language": "语言", - "loading": "加载中", - "modes": { - "Default": "默认", - "Smart": "智能", - "Always": "总是", - "None": "无", - "OnlyForced": "仅强制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自动旋转", - "video_orientation": "视频方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默认", - "ALL": "全部", - "PORTRAIT": "纵向", - "PORTRAIT_UP": "纵向向上", - "PORTRAIT_DOWN": "纵向向下", - "LANDSCAPE": "横向", - "LANDSCAPE_LEFT": "横向左", - "LANDSCAPE_RIGHT": "横向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全区域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "显示自定义菜单链接", - "hide_libraries": "隐藏媒体库", - "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", - "disable_haptic_feedback": "禁用触觉反馈" - }, - "downloads": { - "downloads_title": "下载", - "download_method": "下载方法", - "remux_max_download": "Remux 最大下载", - "auto_download": "自动下载", - "optimized_versions_server": "Optimized Version 服务器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已优化", - "default": "默认", - "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", - "server_url": "服务器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密码", - "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登录", - "total_media_requests": "总媒体请求", - "movie_quota_limit": "电影配额限制", - "movie_quota_days": "电影配额天数", - "tv_quota_limit": "剧集配额限制", - "tv_quota_days": "剧集配额天数", - "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", - "unlimited": "无限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "启用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_marlin": "查看更多关于 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存储", - "app_usage": "应用 {{usedSpace}}%", - "device_usage": "设备 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "删除所有已下载文件" - }, - "intro": { - "show_intro": "显示介绍", - "reset_intro": "重置介绍" - }, - "logs": { - "logs_title": "日志", - "no_logs_available": "无可用日志", - "delete_all_logs": "删除所有日志" - }, - "languages": { - "title": "语言", - "app_language": "应用语言", - "app_language_description": "选择应用的语言。", - "system": "系统" - }, - "toasts": { - "error_deleting_files": "删除文件时出错", - "background_downloads_enabled": "后台下载已启用", - "background_downloads_disabled": "后台下载已禁用", - "connected": "已连接", - "could_not_connect": "无法连接", - "invalid_url": "无效 URL" - } - }, - "downloads": { - "downloads_title": "下载", - "tvseries": "剧集", - "movies": "电影", - "queue": "队列", - "queue_hint": "应用重启后队列和下载将会丢失", - "no_items_in_queue": "队列中无项目", - "no_downloaded_items": "无已下载项目", - "delete_all_movies_button": "删除所有电影", - "delete_all_tvseries_button": "删除所有剧集", - "delete_all_button": "删除全部", - "active_download": "活跃下载", - "no_active_downloads": "无活跃下载", - "active_downloads": "活跃下载", - "new_app_version_requires_re_download": "更新版本需要重新下载", - "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", - "back": "返回", - "delete": "删除", - "something_went_wrong": "出现问题", - "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", - "eta": "预计完成时间 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您无权下载文件。", - "deleted_all_movies_successfully": "成功删除所有电影!", - "failed_to_delete_all_movies": "删除所有电影失败", - "deleted_all_tvseries_successfully": "成功删除所有剧集!", - "failed_to_delete_all_tvseries": "删除所有剧集失败", - "download_cancelled": "下载已取消", - "could_not_cancel_download": "无法取消下载", - "download_completed": "下载完成", - "download_started_for": "开始下载 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", - "download_stated_for_item": "开始下载 {{item}}", - "download_failed_for_item": "下载失败 {{item}} - {{error}}", - "download_completed_for_item": "下载完成 {{item}}", - "queued_item_for_optimization": "已将 {{item}} 队列进行优化", - "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", - "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", - "no_response_received_from_server": "未收到服务器响应", - "error_setting_up_the_request": "设置请求时出错", - "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", - "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", - "go_to_downloads": "前往下载" - } - } - }, - "search": { - "search_here": "在此搜索...", - "search": "搜索...", - "x_items": "{{count}} 项目", - "library": "媒体库", - "discover": "发现", - "no_results": "没有结果", - "no_results_found_for": "未找到结果", - "movies": "电影", - "series": "剧集", - "episodes": "单集", - "collections": "收藏", - "actors": "演员", - "request_movies": "请求电影", - "request_series": "请求系列", - "recently_added": "最近添加", - "recent_requests": "最近请求", - "plex_watchlist": "Plex 观影清单", - "trending": "趋势", - "popular_movies": "热门电影", - "movie_genres": "电影类型", - "upcoming_movies": "即将上映的电影", - "studios": "工作室", - "popular_tv": "热门电影", - "tv_genres": "剧集类型", - "upcoming_tv": "即将上映的剧集", - "networks": "网络", - "tmdb_movie_keyword": "TMDB 电影关键词", - "tmdb_movie_genre": "TMDB 电影类型", - "tmdb_tv_keyword": "TMDB 剧集关键词", - "tmdb_tv_genre": "TMDB 剧集类型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 网络", - "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", - "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" - }, - "library": { - "no_items_found": "未找到项目", - "no_results": "没有结果", - "no_libraries_found": "未找到媒体库", - "item_types": { - "movies": "电影", - "series": "剧集", - "boxsets": "套装", - "items": "项" - }, - "options": { - "display": "显示", - "row": "行", - "list": "列表", - "image_style": "图片样式", - "poster": "海报", - "cover": "封面", - "show_titles": "显示标题", - "show_stats": "显示统计" - }, - "filters": { - "genres": "类型", - "years": "年份", - "sort_by": "排序依据", - "sort_order": "排序顺序", - "asc": "Ascending", - "desc": "Descending", - "tags": "标签" - } - }, - "favorites": { - "series": "剧集", - "movies": "电影", - "episodes": "单集", - "videos": "视频", - "boxsets": "套装", - "playlists": "播放列表", - "noDataTitle": "暂无收藏", - "noData": "将项目标记为收藏,它们将显示在此处以便快速访问。" - }, - "custom_links": { - "no_links": "无链接" - }, - "player": { - "error": "错误", - "failed_to_get_stream_url": "无法获取流 URL", - "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", - "client_error": "客户端错误", - "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", - "message_from_server": "来自服务器的消息", - "video_has_finished_playing": "视频播放完成!", - "no_video_source": "无视频来源...", - "next_episode": "下一集", - "refresh_tracks": "刷新轨道", - "subtitle_tracks": "字幕轨道:", - "audio_tracks": "音频轨道:", - "playback_state": "播放状态:", - "no_data_available": "无可用数据", - "index": "索引:" - }, - "item_card": { - "next_up": "下一个", - "no_items_to_display": "无项目显示", - "cast_and_crew": "演员和工作人员", - "series": "剧集", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季无剧集", - "overview": "概览", - "more_with": "更多 {{name}} 的作品", - "similar_items": "类似项目", - "no_similar_items_found": "未找到类似项目", - "video": "视频", - "more_details": "更多详情", - "quality": "质量", - "audio": "音频", - "subtitles": "字幕", - "show_more": "显示更多", - "show_less": "显示更少", - "appeared_in": "出现于", - "could_not_load_item": "无法加载项目", - "none": "无", - "download": { - "download_season": "下载季", - "download_series": "下载剧集", - "download_episode": "下载单集", - "download_movie": "下载电影", - "download_x_item": "下载 {{item_count}} 项目", - "download_button": "下载", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默认方法" - } - }, - "live_tv": { - "next": "下一个", - "previous": "上一个", - "live_tv": "直播电视", - "coming_soon": "即将播出", - "on_now": "正在播放", - "shows": "节目", - "movies": "电影", - "sports": "体育", - "for_kids": "儿童", - "news": "新闻" - }, - "jellyseerr": { - "confirm": "确认", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什么问题?", - "issue_type": "问题类型", - "select_an_issue": "选择一个问题", - "types": "类型", - "describe_the_issue": "(可选)描述问题...", - "submit_button": "提交", - "report_issue_button": "报告问题", - "request_button": "请求", - "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", - "failed_to_login": "登录失败", - "cast": "演员", - "details": "详情", - "status": "状态", - "original_title": "原标题", - "series_type": "剧集类型", - "release_dates": "发行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "预算", - "original_language": "原始语言", - "production_country": "制作国家/地区", - "studios": "工作室", - "network": "网络", - "currently_streaming_on": "目前在以下流媒体上播放", - "advanced": "高级设置", - "request_as": "选择用户以请求", - "tags": "标签", - "quality_profile": "质量配置文件", - "root_folder": "根文件夹", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出场", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", - "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", - "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", - "issue_submitted": "问题已提交!", - "requested_item": "已请求 {{item}}!", - "you_dont_have_permission_to_request": "您无权请求媒体!", - "something_went_wrong_requesting_media": "请求媒体时出了些问题!" - } - }, - "tabs": { - "home": "主页", - "search": "搜索", - "library": "媒体库", - "custom_links": "自定义链接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用户名", + "error_title": "错误", + "login_title": "登录", + "login_to_title": "登录至", + "username_placeholder": "用户名", + "password_placeholder": "密码", + "login_button": "登录", + "quick_connect": "快速连接", + "enter_code_to_login": "输入代码 {{code}} 以登录", + "failed_to_initiate_quick_connect": "无法启动快速连接", + "got_it": "了解", + "connection_failed": "连接失败", + "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", + "an_unexpected_error_occured": "发生意外错误", + "change_server": "更改服务器", + "invalid_username_or_password": "无效的用户名或密码", + "user_does_not_have_permission_to_log_in": "用户没有登录权限", + "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", + "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", + "there_is_a_server_error": "服务器出错", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "连接", + "previous_servers": "上一个服务器", + "clear_button": "清除", + "search_for_local_servers": "搜索本地服务器", + "searching": "搜索中...", + "servers": "服务器" + }, + "home": { + "no_internet": "无网络", + "no_items": "无项目", + "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", + "go_to_downloads": "前往下载", + "oops": "哎呀!", + "error_message": "出错了。\n请注销重新登录。", + "continue_watching": "继续观看", + "next_up": "下一个", + "recently_added_in": "最近添加于 {{libraryName}}", + "suggested_movies": "推荐电影", + "suggested_episodes": "推荐剧集", + "intro": { + "welcome_to_streamyfin": "欢迎来到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", + "features_title": "功能", + "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", + "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", + "downloads_feature_title": "下载", + "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", + "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", + "centralised_settings_plugin_title": "统一设置插件", + "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", + "done_button": "完成", + "go_to_settings_button": "前往设置", + "read_more": "了解更多" + }, + "settings": { + "settings_title": "设置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用户信息", + "user": "用户", + "server": "服务器", + "token": "密钥", + "app_version": "应用版本" + }, + "quick_connect": { + "quick_connect_title": "快速连接", + "authorize_button": "授权快速连接", + "enter_the_quick_connect_code": "输入快速连接代码...", + "success": "成功", + "quick_connect_autorized": "快速连接已授权", + "error": "错误", + "invalid_code": "无效代码", + "authorize": "授权" + }, + "media_controls": { + "media_controls_title": "媒体控制", + "forward_skip_length": "快进时长", + "rewind_length": "快退时长", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音频", + "set_audio_track": "从上一个项目设置音轨", + "audio_language": "音频语言", + "audio_hint": "选择默认音频语言。", + "none": "无", + "language": "语言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕语言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "从上一个项目设置字幕", + "subtitle_size": "字幕大小", + "subtitle_hint": "设置字幕偏好。", + "none": "无", + "language": "语言", + "loading": "加载中", + "modes": { + "Default": "默认", + "Smart": "智能", + "Always": "总是", + "None": "无", + "OnlyForced": "仅强制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自动旋转", + "video_orientation": "视频方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默认", + "ALL": "全部", + "PORTRAIT": "纵向", + "PORTRAIT_UP": "纵向向上", + "PORTRAIT_DOWN": "纵向向下", + "LANDSCAPE": "横向", + "LANDSCAPE_LEFT": "横向左", + "LANDSCAPE_RIGHT": "横向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全区域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "显示自定义菜单链接", + "hide_libraries": "隐藏媒体库", + "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", + "disable_haptic_feedback": "禁用触觉反馈" + }, + "downloads": { + "downloads_title": "下载", + "download_method": "下载方法", + "remux_max_download": "Remux 最大下载", + "auto_download": "自动下载", + "optimized_versions_server": "Optimized Version 服务器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已优化", + "default": "默认", + "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", + "server_url": "服务器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密码", + "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登录", + "total_media_requests": "总媒体请求", + "movie_quota_limit": "电影配额限制", + "movie_quota_days": "电影配额天数", + "tv_quota_limit": "剧集配额限制", + "tv_quota_days": "剧集配额天数", + "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", + "unlimited": "无限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "启用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_marlin": "查看更多关于 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存储", + "app_usage": "应用 {{usedSpace}}%", + "device_usage": "设备 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "删除所有已下载文件" + }, + "intro": { + "show_intro": "显示介绍", + "reset_intro": "重置介绍" + }, + "logs": { + "logs_title": "日志", + "no_logs_available": "无可用日志", + "delete_all_logs": "删除所有日志" + }, + "languages": { + "title": "语言", + "app_language": "应用语言", + "app_language_description": "选择应用的语言。", + "system": "系统" + }, + "toasts": { + "error_deleting_files": "删除文件时出错", + "background_downloads_enabled": "后台下载已启用", + "background_downloads_disabled": "后台下载已禁用", + "connected": "已连接", + "could_not_connect": "无法连接", + "invalid_url": "无效 URL" + } + }, + "downloads": { + "downloads_title": "下载", + "tvseries": "剧集", + "movies": "电影", + "queue": "队列", + "queue_hint": "应用重启后队列和下载将会丢失", + "no_items_in_queue": "队列中无项目", + "no_downloaded_items": "无已下载项目", + "delete_all_movies_button": "删除所有电影", + "delete_all_tvseries_button": "删除所有剧集", + "delete_all_button": "删除全部", + "active_download": "活跃下载", + "no_active_downloads": "无活跃下载", + "active_downloads": "活跃下载", + "new_app_version_requires_re_download": "更新版本需要重新下载", + "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", + "back": "返回", + "delete": "删除", + "something_went_wrong": "出现问题", + "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", + "eta": "预计完成时间 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您无权下载文件。", + "deleted_all_movies_successfully": "成功删除所有电影!", + "failed_to_delete_all_movies": "删除所有电影失败", + "deleted_all_tvseries_successfully": "成功删除所有剧集!", + "failed_to_delete_all_tvseries": "删除所有剧集失败", + "download_cancelled": "下载已取消", + "could_not_cancel_download": "无法取消下载", + "download_completed": "下载完成", + "download_started_for": "开始下载 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", + "download_stated_for_item": "开始下载 {{item}}", + "download_failed_for_item": "下载失败 {{item}} - {{error}}", + "download_completed_for_item": "下载完成 {{item}}", + "queued_item_for_optimization": "已将 {{item}} 队列进行优化", + "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", + "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", + "no_response_received_from_server": "未收到服务器响应", + "error_setting_up_the_request": "设置请求时出错", + "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", + "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", + "go_to_downloads": "前往下载" + } + } + }, + "search": { + "search_here": "在此搜索...", + "search": "搜索...", + "x_items": "{{count}} 项目", + "library": "媒体库", + "discover": "发现", + "no_results": "没有结果", + "no_results_found_for": "未找到结果", + "movies": "电影", + "series": "剧集", + "episodes": "单集", + "collections": "收藏", + "actors": "演员", + "request_movies": "请求电影", + "request_series": "请求系列", + "recently_added": "最近添加", + "recent_requests": "最近请求", + "plex_watchlist": "Plex 观影清单", + "trending": "趋势", + "popular_movies": "热门电影", + "movie_genres": "电影类型", + "upcoming_movies": "即将上映的电影", + "studios": "工作室", + "popular_tv": "热门电影", + "tv_genres": "剧集类型", + "upcoming_tv": "即将上映的剧集", + "networks": "网络", + "tmdb_movie_keyword": "TMDB 电影关键词", + "tmdb_movie_genre": "TMDB 电影类型", + "tmdb_tv_keyword": "TMDB 剧集关键词", + "tmdb_tv_genre": "TMDB 剧集类型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 网络", + "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", + "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" + }, + "library": { + "no_items_found": "未找到项目", + "no_results": "没有结果", + "no_libraries_found": "未找到媒体库", + "item_types": { + "movies": "电影", + "series": "剧集", + "boxsets": "套装", + "items": "项" + }, + "options": { + "display": "显示", + "row": "行", + "list": "列表", + "image_style": "图片样式", + "poster": "海报", + "cover": "封面", + "show_titles": "显示标题", + "show_stats": "显示统计" + }, + "filters": { + "genres": "类型", + "years": "年份", + "sort_by": "排序依据", + "sort_order": "排序顺序", + "asc": "Ascending", + "desc": "Descending", + "tags": "标签" + } + }, + "favorites": { + "series": "剧集", + "movies": "电影", + "episodes": "单集", + "videos": "视频", + "boxsets": "套装", + "playlists": "播放列表", + "noDataTitle": "暂无收藏", + "noData": "将项目标记为收藏,它们将显示在此处以便快速访问。" + }, + "custom_links": { + "no_links": "无链接" + }, + "player": { + "error": "错误", + "failed_to_get_stream_url": "无法获取流 URL", + "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", + "client_error": "客户端错误", + "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", + "message_from_server": "来自服务器的消息", + "video_has_finished_playing": "视频播放完成!", + "no_video_source": "无视频来源...", + "next_episode": "下一集", + "refresh_tracks": "刷新轨道", + "subtitle_tracks": "字幕轨道:", + "audio_tracks": "音频轨道:", + "playback_state": "播放状态:", + "no_data_available": "无可用数据", + "index": "索引:" + }, + "item_card": { + "next_up": "下一个", + "no_items_to_display": "无项目显示", + "cast_and_crew": "演员和工作人员", + "series": "剧集", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季无剧集", + "overview": "概览", + "more_with": "更多 {{name}} 的作品", + "similar_items": "类似项目", + "no_similar_items_found": "未找到类似项目", + "video": "视频", + "more_details": "更多详情", + "quality": "质量", + "audio": "音频", + "subtitles": "字幕", + "show_more": "显示更多", + "show_less": "显示更少", + "appeared_in": "出现于", + "could_not_load_item": "无法加载项目", + "none": "无", + "download": { + "download_season": "下载季", + "download_series": "下载剧集", + "download_episode": "下载单集", + "download_movie": "下载电影", + "download_x_item": "下载 {{item_count}} 项目", + "download_button": "下载", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默认方法" + } + }, + "live_tv": { + "next": "下一个", + "previous": "上一个", + "live_tv": "直播电视", + "coming_soon": "即将播出", + "on_now": "正在播放", + "shows": "节目", + "movies": "电影", + "sports": "体育", + "for_kids": "儿童", + "news": "新闻" + }, + "jellyseerr": { + "confirm": "确认", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什么问题?", + "issue_type": "问题类型", + "select_an_issue": "选择一个问题", + "types": "类型", + "describe_the_issue": "(可选)描述问题...", + "submit_button": "提交", + "report_issue_button": "报告问题", + "request_button": "请求", + "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", + "failed_to_login": "登录失败", + "cast": "演员", + "details": "详情", + "status": "状态", + "original_title": "原标题", + "series_type": "剧集类型", + "release_dates": "发行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "预算", + "original_language": "原始语言", + "production_country": "制作国家/地区", + "studios": "工作室", + "network": "网络", + "currently_streaming_on": "目前在以下流媒体上播放", + "advanced": "高级设置", + "request_as": "选择用户以请求", + "tags": "标签", + "quality_profile": "质量配置文件", + "root_folder": "根文件夹", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出场", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", + "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", + "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", + "issue_submitted": "问题已提交!", + "requested_item": "已请求 {{item}}!", + "you_dont_have_permission_to_request": "您无权请求媒体!", + "something_went_wrong_requesting_media": "请求媒体时出了些问题!" + } + }, + "tabs": { + "home": "主页", + "search": "搜索", + "library": "媒体库", + "custom_links": "自定义链接", + "favorites": "收藏" + } } diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 5cfb2066..e533b56a 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "需要用戶名", - "error_title": "錯誤", - "login_title": "登入", - "login_to_title": "登入至", - "username_placeholder": "用戶名", - "password_placeholder": "密碼", - "login_button": "登入", - "quick_connect": "快速連接", - "enter_code_to_login": "輸入代碼 {{code}} 以登入", - "failed_to_initiate_quick_connect": "無法啟動快速連接", - "got_it": "知道了", - "connection_failed": "連接失敗", - "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", - "an_unexpected_error_occured": "發生意外錯誤", - "change_server": "更改伺服器", - "invalid_username_or_password": "無效的用戶名或密碼", - "user_does_not_have_permission_to_log_in": "用戶無權登入", - "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", - "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", - "there_is_a_server_error": "伺服器出錯", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "連接", - "previous_servers": "先前的伺服器", - "clear_button": "清除", - "search_for_local_servers": "搜尋本地伺服器", - "searching": "搜尋中...", - "servers": "伺服器" - }, - "home": { - "no_internet": "無網絡", - "no_items": "無項目", - "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", - "go_to_downloads": "前往下載", - "oops": "哎呀!", - "error_message": "出錯了。\n請重新登出並登入。", - "continue_watching": "繼續觀看", - "next_up": "下一個", - "recently_added_in": "最近添加於 {{libraryName}}", - "suggested_movies": "推薦電影", - "suggested_episodes": "推薦劇集", - "intro": { - "welcome_to_streamyfin": "歡迎來到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", - "features_title": "功能", - "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", - "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", - "downloads_feature_title": "下載", - "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", - "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", - "centralised_settings_plugin_title": "統一設置插件", - "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", - "done_button": "完成", - "go_to_settings_button": "前往設置", - "read_more": "閱讀更多" - }, - "settings": { - "settings_title": "設置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用戶信息", - "user": "用戶", - "server": "伺服器", - "token": "令牌", - "app_version": "應用版本" - }, - "quick_connect": { - "quick_connect_title": "快速連接", - "authorize_button": "授權快速連接", - "enter_the_quick_connect_code": "輸入快速連接代碼...", - "success": "成功", - "quick_connect_autorized": "快速連接已授權", - "error": "錯誤", - "invalid_code": "無效代碼", - "authorize": "授權" - }, - "media_controls": { - "media_controls_title": "媒體控制", - "forward_skip_length": "快進秒數", - "rewind_length": "倒帶秒數", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音頻", - "set_audio_track": "從上一個項目設置音軌", - "audio_language": "音頻語言", - "audio_hint": "選擇默認音頻語言。", - "none": "無", - "language": "語言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕語言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "從上一個項目設置字幕軌道", - "subtitle_size": "字幕大小", - "subtitle_hint": "配置字幕偏好。", - "none": "無", - "language": "語言", - "loading": "加載中", - "modes": { - "Default": "默認", - "Smart": "智能", - "Always": "總是", - "None": "無", - "OnlyForced": "僅強制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自動旋轉", - "video_orientation": "影片方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默認", - "ALL": "全部", - "PORTRAIT": "縱向", - "PORTRAIT_UP": "縱向向上", - "PORTRAIT_DOWN": "縱向向下", - "LANDSCAPE": "橫向", - "LANDSCAPE_LEFT": "橫向左", - "LANDSCAPE_RIGHT": "橫向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全區域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "顯示自定義菜單鏈接", - "hide_libraries": "隱藏媒體庫", - "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", - "disable_haptic_feedback": "禁用觸覺回饋" - }, - "downloads": { - "downloads_title": "下載", - "download_method": "下載方法", - "remux_max_download": "Remux 最大下載", - "auto_download": "自動下載", - "optimized_versions_server": "Optimized Version 伺服器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已優化", - "default": "默認", - "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", - "server_url": "伺服器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密碼", - "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登入", - "total_media_requests": "總媒體請求", - "movie_quota_limit": "電影配額限制", - "movie_quota_days": "電影配額天數", - "tv_quota_limit": "電視配額限制", - "tv_quota_days": "電視配額天數", - "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", - "unlimited": "無限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "啟用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存儲", - "app_usage": "應用 {{usedSpace}}%", - "device_usage": "設備 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "刪除所有已下載文件" - }, - "intro": { - "show_intro": "顯示介紹", - "reset_intro": "重置介紹" - }, - "logs": { - "logs_title": "日誌", - "no_logs_available": "無可用日誌", - "delete_all_logs": "刪除所有日誌" - }, - "languages": { - "title": "語言", - "app_language": "應用語言", - "app_language_description": "選擇應用的語言。", - "system": "系統" - }, - "toasts": { - "error_deleting_files": "刪除文件時出錯", - "background_downloads_enabled": "背景下載已啟用", - "background_downloads_disabled": "背景下載已禁用", - "connected": "已連接", - "could_not_connect": "無法連接", - "invalid_url": "無效的 URL" - } - }, - "downloads": { - "downloads_title": "下載", - "tvseries": "電視劇", - "movies": "電影", - "queue": "隊列", - "queue_hint": "應用重啟後隊列和下載將會丟失", - "no_items_in_queue": "隊列中無項目", - "no_downloaded_items": "無已下載項目", - "delete_all_movies_button": "刪除所有電影", - "delete_all_tvseries_button": "刪除所有電視劇", - "delete_all_button": "刪除全部", - "active_download": "活動下載", - "no_active_downloads": "無活動下載", - "active_downloads": "活動下載", - "new_app_version_requires_re_download": "新應用版本需要重新下載", - "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", - "back": "返回", - "delete": "刪除", - "something_went_wrong": "出了些問題", - "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", - "eta": "預計完成時間 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您無權下載文件。", - "deleted_all_movies_successfully": "成功刪除所有電影!", - "failed_to_delete_all_movies": "刪除所有電影失敗", - "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", - "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", - "download_cancelled": "下載已取消", - "could_not_cancel_download": "無法取消下載", - "download_completed": "下載完成", - "download_started_for": "開始下載 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", - "download_stated_for_item": "開始下載 {{item}}", - "download_failed_for_item": "下載失敗 {{item}} - {{error}}", - "download_completed_for_item": "下載完成 {{item}}", - "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", - "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", - "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", - "no_response_received_from_server": "未收到伺服器的響應", - "error_setting_up_the_request": "設置請求時出錯", - "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", - "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", - "go_to_downloads": "前往下載" - } - } - }, - "search": { - "search_here": "在這裡搜索...", - "search": "搜索...", - "x_items": "{{count}} 項目", - "library": "媒體庫", - "discover": "發現", - "no_results": "沒有結果", - "no_results_found_for": "未找到結果", - "movies": "電影", - "series": "系列", - "episodes": "劇集", - "collections": "收藏", - "actors": "演員", - "request_movies": "請求電影", - "request_series": "請求系列", - "recently_added": "最近添加", - "recent_requests": "最近請求", - "plex_watchlist": "Plex 觀影清單", - "trending": "趨勢", - "popular_movies": "熱門電影", - "movie_genres": "電影類型", - "upcoming_movies": "即將上映的電影", - "studios": "工作室", - "popular_tv": "熱門電視", - "tv_genres": "電視類型", - "upcoming_tv": "即將上映的電視", - "networks": "網絡", - "tmdb_movie_keyword": "TMDB 電影關鍵詞", - "tmdb_movie_genre": "TMDB 電影類型", - "tmdb_tv_keyword": "TMDB 電視關鍵詞", - "tmdb_tv_genre": "TMDB 電視類型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 網絡", - "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", - "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" - }, - "library": { - "no_items_found": "未找到項目", - "no_results": "沒有結果", - "no_libraries_found": "未找到媒體庫", - "item_types": { - "movies": "電影", - "series": "系列", - "boxsets": "套裝", - "items": "項目" - }, - "options": { - "display": "顯示", - "row": "行", - "list": "列表", - "image_style": "圖片樣式", - "poster": "海報", - "cover": "封面", - "show_titles": "顯示標題", - "show_stats": "顯示統計" - }, - "filters": { - "genres": "類型", - "years": "年份", - "sort_by": "排序依據", - "sort_order": "排序順序", - "asc": "Ascending", - "desc": "Descending", - "tags": "標籤" - } - }, - "favorites": { - "series": "系列", - "movies": "電影", - "episodes": "劇集", - "videos": "影片", - "boxsets": "套裝", - "playlists": "播放列表", - "noDataTitle": "尚無收藏", - "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" - }, - "custom_links": { - "no_links": "無鏈接" - }, - "player": { - "error": "錯誤", - "failed_to_get_stream_url": "無法獲取流 URL", - "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", - "client_error": "客戶端錯誤", - "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", - "message_from_server": "來自伺服器的消息", - "video_has_finished_playing": "影片播放完畢!", - "no_video_source": "無影片來源...", - "next_episode": "下一集", - "refresh_tracks": "刷新軌道", - "subtitle_tracks": "字幕軌道:", - "audio_tracks": "音頻軌道:", - "playback_state": "播放狀態:", - "no_data_available": "無可用數據", - "index": "索引:" - }, - "item_card": { - "next_up": "下一個", - "no_items_to_display": "無項目顯示", - "cast_and_crew": "演員和工作人員", - "series": "系列", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季無劇集", - "overview": "概覽", - "more_with": "更多 {{name}} 的作品", - "similar_items": "類似項目", - "no_similar_items_found": "未找到類似項目", - "video": "影片", - "more_details": "更多詳情", - "quality": "質量", - "audio": "音頻", - "subtitles": "字幕", - "show_more": "顯示更多", - "show_less": "顯示更少", - "appeared_in": "出現於", - "could_not_load_item": "無法加載項目", - "none": "無", - "download": { - "download_season": "下載季度", - "download_series": "下載系列", - "download_episode": "下載劇集", - "download_movie": "下載電影", - "download_x_item": "下載 {{item_count}} 項目", - "download_button": "下載", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默認方法" - } - }, - "live_tv": { - "next": "下一個", - "previous": "上一個", - "live_tv": "直播電視", - "coming_soon": "即將推出", - "on_now": "正在播放", - "shows": "節目", - "movies": "電影", - "sports": "體育", - "for_kids": "兒童", - "news": "新聞" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什麼問題?", - "issue_type": "問題類型", - "select_an_issue": "選擇一個問題", - "types": "類型", - "describe_the_issue": "(可選)描述問題...", - "submit_button": "提交", - "report_issue_button": "報告問題", - "request_button": "請求", - "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", - "failed_to_login": "登入失敗", - "cast": "演員", - "details": "詳情", - "status": "狀態", - "original_title": "原標題", - "series_type": "系列類型", - "release_dates": "發行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "預算", - "original_language": "原始語言", - "production_country": "製作國家", - "studios": "工作室", - "network": "網絡", - "currently_streaming_on": "目前在以下流媒體上播放", - "advanced": "高級設定", - "request_as": "選擇用戶以作請求", - "tags": "標籤", - "quality_profile": "質量配置文件", - "root_folder": "根文件夾", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出場", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", - "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", - "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", - "issue_submitted": "問題已提交!", - "requested_item": "已請求 {{item}}!", - "you_dont_have_permission_to_request": "您無權請求媒體!", - "something_went_wrong_requesting_media": "請求媒體時出了些問題!" - } - }, - "tabs": { - "home": "主頁", - "search": "搜索", - "library": "媒體庫", - "custom_links": "自定義鏈接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用戶名", + "error_title": "錯誤", + "login_title": "登入", + "login_to_title": "登入至", + "username_placeholder": "用戶名", + "password_placeholder": "密碼", + "login_button": "登入", + "quick_connect": "快速連接", + "enter_code_to_login": "輸入代碼 {{code}} 以登入", + "failed_to_initiate_quick_connect": "無法啟動快速連接", + "got_it": "知道了", + "connection_failed": "連接失敗", + "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", + "an_unexpected_error_occured": "發生意外錯誤", + "change_server": "更改伺服器", + "invalid_username_or_password": "無效的用戶名或密碼", + "user_does_not_have_permission_to_log_in": "用戶無權登入", + "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", + "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", + "there_is_a_server_error": "伺服器出錯", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "連接", + "previous_servers": "先前的伺服器", + "clear_button": "清除", + "search_for_local_servers": "搜尋本地伺服器", + "searching": "搜尋中...", + "servers": "伺服器" + }, + "home": { + "no_internet": "無網絡", + "no_items": "無項目", + "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", + "go_to_downloads": "前往下載", + "oops": "哎呀!", + "error_message": "出錯了。\n請重新登出並登入。", + "continue_watching": "繼續觀看", + "next_up": "下一個", + "recently_added_in": "最近添加於 {{libraryName}}", + "suggested_movies": "推薦電影", + "suggested_episodes": "推薦劇集", + "intro": { + "welcome_to_streamyfin": "歡迎來到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", + "features_title": "功能", + "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", + "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", + "downloads_feature_title": "下載", + "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", + "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", + "centralised_settings_plugin_title": "統一設置插件", + "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", + "done_button": "完成", + "go_to_settings_button": "前往設置", + "read_more": "閱讀更多" + }, + "settings": { + "settings_title": "設置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用戶信息", + "user": "用戶", + "server": "伺服器", + "token": "令牌", + "app_version": "應用版本" + }, + "quick_connect": { + "quick_connect_title": "快速連接", + "authorize_button": "授權快速連接", + "enter_the_quick_connect_code": "輸入快速連接代碼...", + "success": "成功", + "quick_connect_autorized": "快速連接已授權", + "error": "錯誤", + "invalid_code": "無效代碼", + "authorize": "授權" + }, + "media_controls": { + "media_controls_title": "媒體控制", + "forward_skip_length": "快進秒數", + "rewind_length": "倒帶秒數", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音頻", + "set_audio_track": "從上一個項目設置音軌", + "audio_language": "音頻語言", + "audio_hint": "選擇默認音頻語言。", + "none": "無", + "language": "語言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕語言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "從上一個項目設置字幕軌道", + "subtitle_size": "字幕大小", + "subtitle_hint": "配置字幕偏好。", + "none": "無", + "language": "語言", + "loading": "加載中", + "modes": { + "Default": "默認", + "Smart": "智能", + "Always": "總是", + "None": "無", + "OnlyForced": "僅強制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自動旋轉", + "video_orientation": "影片方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默認", + "ALL": "全部", + "PORTRAIT": "縱向", + "PORTRAIT_UP": "縱向向上", + "PORTRAIT_DOWN": "縱向向下", + "LANDSCAPE": "橫向", + "LANDSCAPE_LEFT": "橫向左", + "LANDSCAPE_RIGHT": "橫向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全區域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "顯示自定義菜單鏈接", + "hide_libraries": "隱藏媒體庫", + "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", + "disable_haptic_feedback": "禁用觸覺回饋" + }, + "downloads": { + "downloads_title": "下載", + "download_method": "下載方法", + "remux_max_download": "Remux 最大下載", + "auto_download": "自動下載", + "optimized_versions_server": "Optimized Version 伺服器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已優化", + "default": "默認", + "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", + "server_url": "伺服器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密碼", + "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登入", + "total_media_requests": "總媒體請求", + "movie_quota_limit": "電影配額限制", + "movie_quota_days": "電影配額天數", + "tv_quota_limit": "電視配額限制", + "tv_quota_days": "電視配額天數", + "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", + "unlimited": "無限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "啟用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存儲", + "app_usage": "應用 {{usedSpace}}%", + "device_usage": "設備 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "刪除所有已下載文件" + }, + "intro": { + "show_intro": "顯示介紹", + "reset_intro": "重置介紹" + }, + "logs": { + "logs_title": "日誌", + "no_logs_available": "無可用日誌", + "delete_all_logs": "刪除所有日誌" + }, + "languages": { + "title": "語言", + "app_language": "應用語言", + "app_language_description": "選擇應用的語言。", + "system": "系統" + }, + "toasts": { + "error_deleting_files": "刪除文件時出錯", + "background_downloads_enabled": "背景下載已啟用", + "background_downloads_disabled": "背景下載已禁用", + "connected": "已連接", + "could_not_connect": "無法連接", + "invalid_url": "無效的 URL" + } + }, + "downloads": { + "downloads_title": "下載", + "tvseries": "電視劇", + "movies": "電影", + "queue": "隊列", + "queue_hint": "應用重啟後隊列和下載將會丟失", + "no_items_in_queue": "隊列中無項目", + "no_downloaded_items": "無已下載項目", + "delete_all_movies_button": "刪除所有電影", + "delete_all_tvseries_button": "刪除所有電視劇", + "delete_all_button": "刪除全部", + "active_download": "活動下載", + "no_active_downloads": "無活動下載", + "active_downloads": "活動下載", + "new_app_version_requires_re_download": "新應用版本需要重新下載", + "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", + "back": "返回", + "delete": "刪除", + "something_went_wrong": "出了些問題", + "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", + "eta": "預計完成時間 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您無權下載文件。", + "deleted_all_movies_successfully": "成功刪除所有電影!", + "failed_to_delete_all_movies": "刪除所有電影失敗", + "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", + "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", + "download_cancelled": "下載已取消", + "could_not_cancel_download": "無法取消下載", + "download_completed": "下載完成", + "download_started_for": "開始下載 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", + "download_stated_for_item": "開始下載 {{item}}", + "download_failed_for_item": "下載失敗 {{item}} - {{error}}", + "download_completed_for_item": "下載完成 {{item}}", + "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", + "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", + "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", + "no_response_received_from_server": "未收到伺服器的響應", + "error_setting_up_the_request": "設置請求時出錯", + "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", + "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", + "go_to_downloads": "前往下載" + } + } + }, + "search": { + "search_here": "在這裡搜索...", + "search": "搜索...", + "x_items": "{{count}} 項目", + "library": "媒體庫", + "discover": "發現", + "no_results": "沒有結果", + "no_results_found_for": "未找到結果", + "movies": "電影", + "series": "系列", + "episodes": "劇集", + "collections": "收藏", + "actors": "演員", + "request_movies": "請求電影", + "request_series": "請求系列", + "recently_added": "最近添加", + "recent_requests": "最近請求", + "plex_watchlist": "Plex 觀影清單", + "trending": "趨勢", + "popular_movies": "熱門電影", + "movie_genres": "電影類型", + "upcoming_movies": "即將上映的電影", + "studios": "工作室", + "popular_tv": "熱門電視", + "tv_genres": "電視類型", + "upcoming_tv": "即將上映的電視", + "networks": "網絡", + "tmdb_movie_keyword": "TMDB 電影關鍵詞", + "tmdb_movie_genre": "TMDB 電影類型", + "tmdb_tv_keyword": "TMDB 電視關鍵詞", + "tmdb_tv_genre": "TMDB 電視類型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 網絡", + "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", + "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" + }, + "library": { + "no_items_found": "未找到項目", + "no_results": "沒有結果", + "no_libraries_found": "未找到媒體庫", + "item_types": { + "movies": "電影", + "series": "系列", + "boxsets": "套裝", + "items": "項目" + }, + "options": { + "display": "顯示", + "row": "行", + "list": "列表", + "image_style": "圖片樣式", + "poster": "海報", + "cover": "封面", + "show_titles": "顯示標題", + "show_stats": "顯示統計" + }, + "filters": { + "genres": "類型", + "years": "年份", + "sort_by": "排序依據", + "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", + "tags": "標籤" + } + }, + "favorites": { + "series": "系列", + "movies": "電影", + "episodes": "劇集", + "videos": "影片", + "boxsets": "套裝", + "playlists": "播放列表", + "noDataTitle": "尚無收藏", + "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" + }, + "custom_links": { + "no_links": "無鏈接" + }, + "player": { + "error": "錯誤", + "failed_to_get_stream_url": "無法獲取流 URL", + "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", + "client_error": "客戶端錯誤", + "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", + "message_from_server": "來自伺服器的消息", + "video_has_finished_playing": "影片播放完畢!", + "no_video_source": "無影片來源...", + "next_episode": "下一集", + "refresh_tracks": "刷新軌道", + "subtitle_tracks": "字幕軌道:", + "audio_tracks": "音頻軌道:", + "playback_state": "播放狀態:", + "no_data_available": "無可用數據", + "index": "索引:" + }, + "item_card": { + "next_up": "下一個", + "no_items_to_display": "無項目顯示", + "cast_and_crew": "演員和工作人員", + "series": "系列", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季無劇集", + "overview": "概覽", + "more_with": "更多 {{name}} 的作品", + "similar_items": "類似項目", + "no_similar_items_found": "未找到類似項目", + "video": "影片", + "more_details": "更多詳情", + "quality": "質量", + "audio": "音頻", + "subtitles": "字幕", + "show_more": "顯示更多", + "show_less": "顯示更少", + "appeared_in": "出現於", + "could_not_load_item": "無法加載項目", + "none": "無", + "download": { + "download_season": "下載季度", + "download_series": "下載系列", + "download_episode": "下載劇集", + "download_movie": "下載電影", + "download_x_item": "下載 {{item_count}} 項目", + "download_button": "下載", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默認方法" + } + }, + "live_tv": { + "next": "下一個", + "previous": "上一個", + "live_tv": "直播電視", + "coming_soon": "即將推出", + "on_now": "正在播放", + "shows": "節目", + "movies": "電影", + "sports": "體育", + "for_kids": "兒童", + "news": "新聞" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什麼問題?", + "issue_type": "問題類型", + "select_an_issue": "選擇一個問題", + "types": "類型", + "describe_the_issue": "(可選)描述問題...", + "submit_button": "提交", + "report_issue_button": "報告問題", + "request_button": "請求", + "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", + "failed_to_login": "登入失敗", + "cast": "演員", + "details": "詳情", + "status": "狀態", + "original_title": "原標題", + "series_type": "系列類型", + "release_dates": "發行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "預算", + "original_language": "原始語言", + "production_country": "製作國家", + "studios": "工作室", + "network": "網絡", + "currently_streaming_on": "目前在以下流媒體上播放", + "advanced": "高級設定", + "request_as": "選擇用戶以作請求", + "tags": "標籤", + "quality_profile": "質量配置文件", + "root_folder": "根文件夾", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出場", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", + "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", + "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", + "issue_submitted": "問題已提交!", + "requested_item": "已請求 {{item}}!", + "you_dont_have_permission_to_request": "您無權請求媒體!", + "something_went_wrong_requesting_media": "請求媒體時出了些問題!" + } + }, + "tabs": { + "home": "主頁", + "search": "搜索", + "library": "媒體庫", + "custom_links": "自定義鏈接", + "favorites": "收藏" + } } diff --git a/types.d.ts b/types.d.ts index 8be427bb..684f73c5 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,9 +1,9 @@ declare module "*.svg" { - const content: any; - export default content; + const content: any; + export default content; } declare module "*.png" { - const value: any; - export default value; + const value: any; + export default value; } diff --git a/utils/OrientationLockConverter.ts b/utils/OrientationLockConverter.ts index 498e01cb..29d6c802 100644 --- a/utils/OrientationLockConverter.ts +++ b/utils/OrientationLockConverter.ts @@ -4,7 +4,7 @@ import { } from "@/packages/expo-screen-orientation"; function orientationToOrientationLock( - orientation: Orientation + orientation: Orientation, ): OrientationLock { switch (orientation) { case Orientation.PORTRAIT_UP: diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_jellyseerr/useJellyseerrCanRequest.ts index c58a8928..22439b92 100644 --- a/utils/_jellyseerr/useJellyseerrCanRequest.ts +++ b/utils/_jellyseerr/useJellyseerrCanRequest.ts @@ -4,17 +4,20 @@ import { MediaStatus, } from "@/utils/jellyseerr/server/constants/media"; import { - hasPermission, Permission, + hasPermission, } from "@/utils/jellyseerr/server/lib/permissions"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { useMemo } from "react"; -import MediaRequest from "../jellyseerr/server/entity/MediaRequest"; -import { MovieDetails } from "../jellyseerr/server/models/Movie"; -import { TvDetails } from "../jellyseerr/server/models/Tv"; +import type MediaRequest from "../jellyseerr/server/entity/MediaRequest"; +import type { MovieDetails } from "../jellyseerr/server/models/Movie"; +import type { TvDetails } from "../jellyseerr/server/models/Tv"; export const useJellyseerrCanRequest = ( - item?: MovieResult | TvResult | MovieDetails | TvDetails + item?: MovieResult | TvResult | MovieDetails | TvDetails, ) => { const { jellyseerrUser } = useJellyseerr(); @@ -25,7 +28,7 @@ export const useJellyseerrCanRequest = ( item?.mediaInfo?.requests?.some( (r: MediaRequest) => r.status == MediaRequestStatus.PENDING || - r.status == MediaRequestStatus.APPROVED + r.status == MediaRequestStatus.APPROVED, ) || item.mediaInfo?.status === MediaStatus.AVAILABLE || item.mediaInfo?.status === MediaStatus.BLACKLISTED || @@ -42,26 +45,21 @@ export const useJellyseerrCanRequest = ( : Permission.REQUEST_TV, ], jellyseerrUser.permissions, - { type: "or" } + { type: "or" }, ); return userHasPermission && !canNotRequest; }, [item, jellyseerrUser]); const hasAdvancedRequestPermission = useMemo(() => { - if (!jellyseerrUser) return false; + if (!jellyseerrUser) return false; - return hasPermission( - [ - Permission.REQUEST_ADVANCED, - Permission.MANAGE_REQUESTS - ], - jellyseerrUser.permissions, - {type: 'or'} - ) - }, - [jellyseerrUser] - ); + return hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + jellyseerrUser.permissions, + { type: "or" }, + ); + }, [jellyseerrUser]); return [canRequest, hasAdvancedRequestPermission]; }; diff --git a/utils/atoms/filters.ts b/utils/atoms/filters.ts index e2c9e60c..df9c5c78 100644 --- a/utils/atoms/filters.ts +++ b/utils/atoms/filters.ts @@ -93,7 +93,7 @@ export const sortByPreferenceAtom = atomWithStorage( removeItem: (key) => { storage.delete(key); }, - } + }, ); export const sortOrderPreferenceAtom = atomWithStorage( @@ -110,19 +110,19 @@ export const sortOrderPreferenceAtom = atomWithStorage( removeItem: (key) => { storage.delete(key); }, - } + }, ); export const getSortByPreference = ( libraryId: string, - preferences: SortPreference + preferences: SortPreference, ) => { return preferences?.[libraryId] || null; }; export const getSortOrderPreference = ( libraryId: string, - preferences: SortOrderPreference + preferences: SortOrderPreference, ) => { return preferences?.[libraryId] || null; }; diff --git a/utils/atoms/orientation.ts b/utils/atoms/orientation.ts index 4ee340a2..42f21b3a 100644 --- a/utils/atoms/orientation.ts +++ b/utils/atoms/orientation.ts @@ -2,5 +2,5 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { atom } from "jotai"; export const orientationAtom = atom( - ScreenOrientation.OrientationLock.PORTRAIT_UP + ScreenOrientation.OrientationLock.PORTRAIT_UP, ); diff --git a/utils/atoms/primaryColor.ts b/utils/atoms/primaryColor.ts index cd348ca5..b2574262 100644 --- a/utils/atoms/primaryColor.ts +++ b/utils/atoms/primaryColor.ts @@ -7,9 +7,9 @@ interface ThemeColors { export const calculateTextColor = (backgroundColor: string): string => { // Convert hex to RGB - const r = parseInt(backgroundColor.slice(1, 3), 16); - const g = parseInt(backgroundColor.slice(3, 5), 16); - const b = parseInt(backgroundColor.slice(5, 7), 16); + const r = Number.parseInt(backgroundColor.slice(1, 3), 16); + const g = Number.parseInt(backgroundColor.slice(3, 5), 16); + const b = Number.parseInt(backgroundColor.slice(5, 7), 16); // Calculate perceived brightness // Using the formula: (R * 299 + G * 587 + B * 114) / 1000 @@ -47,9 +47,9 @@ const calculateRelativeLuminance = (rgb: number[]): number => { }; export const isCloseToBlack = (color: string): boolean => { - const r = parseInt(color.slice(1, 3), 16); - const g = parseInt(color.slice(3, 5), 16); - const b = parseInt(color.slice(5, 7), 16); + const r = Number.parseInt(color.slice(1, 3), 16); + const g = Number.parseInt(color.slice(3, 5), 16); + const b = Number.parseInt(color.slice(5, 7), 16); // Check if the color is very dark (close to black) return r < 20 && g < 20 && b < 20; diff --git a/utils/atoms/queue.ts b/utils/atoms/queue.ts index 70f85de9..573d964f 100644 --- a/utils/atoms/queue.ts +++ b/utils/atoms/queue.ts @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { processesAtom } from "@/providers/DownloadProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import type { JobStatus } from "@/utils/optimize-server"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { atom, useAtom } from "jotai"; import { useEffect } from "react"; -import {JobStatus} from "@/utils/optimize-server"; -import {processesAtom} from "@/providers/DownloadProvider"; -import {useSettings} from "@/utils/atoms/settings"; export interface Job { id: string; @@ -24,7 +24,7 @@ export const queueActions = { processJob: async ( queue: Job[], setQueue: (update: Job[]) => void, - setProcessing: (processing: boolean) => void + setProcessing: (processing: boolean) => void, ) => { const [job, ...rest] = queue; @@ -45,7 +45,7 @@ export const queueActions = { }, clear: ( setQueue: (update: Job[]) => void, - setProcessing: (processing: boolean) => void + setProcessing: (processing: boolean) => void, ) => { setQueue([]); setProcessing(false); @@ -59,7 +59,12 @@ export const useJobProcessor = () => { const [settings] = useSettings(); useEffect(() => { - if (!running && queue.length > 0 && settings && processes.length < settings?.remuxConcurrentLimit) { + if ( + !running && + queue.length > 0 && + settings && + processes.length < settings?.remuxConcurrentLimit + ) { console.info("Processing queue", queue); queueActions.processJob(queue, setQueue, setRunning); } diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 50046a7e..bd7b9ab2 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,20 +1,20 @@ +import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { Video } from "@/utils/jellyseerr/server/models/Movie"; +import { writeInfoLog } from "@/utils/log"; +import { + type BaseItemKind, + type CultureDto, + type ItemFilter, + type ItemSortBy, + type SortOrder, + SubtitlePlaybackMode, +} from "@jellyfin/sdk/lib/generated-client"; import { atom, useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { storage } from "../mmkv"; import { Platform } from "react-native"; -import { - CultureDto, - SubtitlePlaybackMode, - ItemSortBy, - SortOrder, - BaseItemKind, - ItemFilter, -} from "@jellyfin/sdk/lib/generated-client"; -import { Bitrate, BITRATES } from "@/components/BitrateSelector"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { writeInfoLog } from "@/utils/log"; -import { Video } from "@/utils/jellyseerr/server/models/Movie"; +import { storage } from "../mmkv"; const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; @@ -26,17 +26,30 @@ export type DownloadOption = { value: DownloadQuality; }; -export const ScreenOrientationEnum: Record = { - [ScreenOrientation.OrientationLock.DEFAULT]: "home.settings.other.orientations.DEFAULT", - [ScreenOrientation.OrientationLock.ALL]: "home.settings.other.orientations.ALL", - [ScreenOrientation.OrientationLock.PORTRAIT]: "home.settings.other.orientations.PORTRAIT", - [ScreenOrientation.OrientationLock.PORTRAIT_UP]: "home.settings.other.orientations.PORTRAIT_UP", - [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "home.settings.other.orientations.PORTRAIT_DOWN", - [ScreenOrientation.OrientationLock.LANDSCAPE]: "home.settings.other.orientations.LANDSCAPE", - [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "home.settings.other.orientations.LANDSCAPE_LEFT", - [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "home.settings.other.orientations.LANDSCAPE_RIGHT", - [ScreenOrientation.OrientationLock.OTHER]: "home.settings.other.orientations.OTHER", - [ScreenOrientation.OrientationLock.UNKNOWN]: "home.settings.other.orientations.UNKNOWN", +export const ScreenOrientationEnum: Record< + ScreenOrientation.OrientationLock, + string +> = { + [ScreenOrientation.OrientationLock.DEFAULT]: + "home.settings.other.orientations.DEFAULT", + [ScreenOrientation.OrientationLock.ALL]: + "home.settings.other.orientations.ALL", + [ScreenOrientation.OrientationLock.PORTRAIT]: + "home.settings.other.orientations.PORTRAIT", + [ScreenOrientation.OrientationLock.PORTRAIT_UP]: + "home.settings.other.orientations.PORTRAIT_UP", + [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: + "home.settings.other.orientations.PORTRAIT_DOWN", + [ScreenOrientation.OrientationLock.LANDSCAPE]: + "home.settings.other.orientations.LANDSCAPE", + [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: + "home.settings.other.orientations.LANDSCAPE_LEFT", + [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: + "home.settings.other.orientations.LANDSCAPE_RIGHT", + [ScreenOrientation.OrientationLock.OTHER]: + "home.settings.other.orientations.OTHER", + [ScreenOrientation.OrientationLock.UNKNOWN]: + "home.settings.other.orientations.UNKNOWN", }; export const DownloadOptions: DownloadOption[] = [ @@ -102,8 +115,8 @@ export type HomeSectionNextUpResolver = { export enum VideoPlayer { // NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted - VLC_3, - VLC_4, + VLC_3 = 0, + VLC_4 = 1, } export type Settings = { @@ -201,7 +214,8 @@ const defaultValues: Settings = { const loadSettings = (): Partial => { try { const jsonValue = storage.getString("settings"); - const loadedValues: Partial = jsonValue != null ? JSON.parse(jsonValue) : {}; + const loadedValues: Partial = + jsonValue != null ? JSON.parse(jsonValue) : {}; return loadedValues; } catch (error) { @@ -223,7 +237,9 @@ const saveSettings = (settings: Settings) => { }; export const settingsAtom = atom | null>(null); -export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); +export const pluginSettingsAtom = atom( + storage.get(STREAMYFIN_PLUGIN_SETTINGS), +); export const useSettings = () => { const api = useAtomValue(apiAtom); @@ -242,7 +258,7 @@ export const useSettings = () => { storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings); _setPluginSettings(settings); }, - [_setPluginSettings] + [_setPluginSettings], ); const refreshStreamyfinPluginSettings = useCallback(async () => { @@ -252,7 +268,7 @@ export const useSettings = () => { writeInfoLog(`Got remote settings: ${data?.settings}`); return data?.settings; }, - (err) => undefined + (err) => undefined, ); setPluginSettings(settings); return settings; @@ -260,11 +276,17 @@ export const useSettings = () => { const updateSettings = (update: Partial) => { if (!_settings) return; - const hasChanges = Object.entries(update).some(([key, value]) => _settings[key as keyof Settings] !== value); + const hasChanges = Object.entries(update).some( + ([key, value]) => _settings[key as keyof Settings] !== value, + ); if (hasChanges) { // Merge default settings, current settings, and updates to ensure all required properties exist - const newSettings = { ...defaultValues, ..._settings, ...update } as Settings; + const newSettings = { + ...defaultValues, + ..._settings, + ...update, + } as Settings; setSettings(newSettings); saveSettings(newSettings); } @@ -275,24 +297,33 @@ export const useSettings = () => { // use user settings first and fallback on admin setting if required. const settings: Settings = useMemo(() => { let unlockedPluginDefaults = {} as Settings; - const overrideSettings = Object.entries(pluginSettings || {}).reduce((acc, [key, setting]) => { - if (setting) { - const { value, locked } = setting; + const overrideSettings = Object.entries(pluginSettings || {}).reduce( + (acc, [key, setting]) => { + if (setting) { + const { value, locked } = setting; - // Make sure we override default settings with plugin settings when they are not locked. - // Admin decided what users defaults should be and grants them the ability to change them too. - if (locked === false && value && _settings?.[key as keyof Settings] !== value) { - unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { - [key as keyof Settings]: value, + // Make sure we override default settings with plugin settings when they are not locked. + // Admin decided what users defaults should be and grants them the ability to change them too. + if ( + locked === false && + value && + _settings?.[key as keyof Settings] !== value + ) { + unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { + [key as keyof Settings]: value, + }); + } + + acc = Object.assign(acc, { + [key]: locked + ? value + : (_settings?.[key as keyof Settings] ?? value), }); } - - acc = Object.assign(acc, { - [key]: locked ? value : _settings?.[key as keyof Settings] ?? value, - }); - } - return acc; - }, {} as Settings); + return acc; + }, + {} as Settings, + ); return { ...defaultValues, @@ -301,5 +332,11 @@ export const useSettings = () => { }; }, [_settings, pluginSettings]); - return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const; + return [ + settings, + updateSettings, + pluginSettings, + setPluginSettings, + refreshStreamyfinPluginSettings, + ] as const; }; diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 6f92d83f..eb01c2c0 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -25,8 +25,7 @@ export async function unregisterBackgroundFetchAsync() { } } -export const BACKGROUND_FETCH_TASK_SESSIONS = - "background-fetch-sessions"; +export const BACKGROUND_FETCH_TASK_SESSIONS = "background-fetch-sessions"; export async function registerBackgroundFetchAsyncSessions() { try { @@ -47,4 +46,4 @@ export async function unregisterBackgroundFetchAsyncSessions() { } catch (error) { console.log("Error unregistering background fetch task", error); } -} \ No newline at end of file +} diff --git a/utils/bitrate.ts b/utils/bitrate.ts index 7f1d0f47..1bd4db6d 100644 --- a/utils/bitrate.ts +++ b/utils/bitrate.ts @@ -3,6 +3,8 @@ export const formatBitrate = (bitrate?: number | null) => { const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; if (bitrate === 0) return "0 bps"; - const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString()); + const i = Number.parseInt( + Math.floor(Math.log(bitrate) / Math.log(1000)).toString(), + ); return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i]; }; diff --git a/utils/collectionTypeToItemType.ts b/utils/collectionTypeToItemType.ts index f37fe5f4..b4f1d58e 100644 --- a/utils/collectionTypeToItemType.ts +++ b/utils/collectionTypeToItemType.ts @@ -20,7 +20,7 @@ import { readonly Folders: "folders"; */ export const colletionTypeToItemType = ( - collectionType?: CollectionType | null + collectionType?: CollectionType | null, ): BaseItemKind | undefined => { if (!collectionType) return undefined; diff --git a/utils/device.ts b/utils/device.ts index 29a988a5..d49ffc67 100644 --- a/utils/device.ts +++ b/utils/device.ts @@ -13,7 +13,7 @@ export const getOrSetDeviceId = () => { }; export const getDeviceId = () => { - let deviceId = storage.getString("deviceId"); + const deviceId = storage.getString("deviceId"); return deviceId || null; }; diff --git a/utils/download.ts b/utils/download.ts index 94a06b7a..547aa15a 100644 --- a/utils/download.ts +++ b/utils/download.ts @@ -2,7 +2,7 @@ import useImageStorage from "@/hooks/useImageStorage"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { storage } from "@/utils/mmkv"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtom } from "jotai"; const useDownloadHelper = () => { @@ -19,7 +19,7 @@ const useDownloadHelper = () => { console.log(`Saving primary image for series: ${item.SeriesId}`); await saveImage( item.SeriesId, - getPrimaryImageUrlById({ api, id: item.SeriesId }) + getPrimaryImageUrlById({ api, id: item.SeriesId }), ); console.log(`Primary image saved for series: ${item.SeriesId}`); } else { diff --git a/utils/eventBus.ts b/utils/eventBus.ts index 4df6b19f..e4bd0fc1 100644 --- a/utils/eventBus.ts +++ b/utils/eventBus.ts @@ -14,7 +14,7 @@ class EventBus { off(event: string, callback: Listener): void { if (!this.listeners[event]) return; this.listeners[event] = this.listeners[event].filter( - (fn) => fn !== callback + (fn) => fn !== callback, ); } diff --git a/utils/getItemImage.ts b/utils/getItemImage.ts index d106b0cf..747beec8 100644 --- a/utils/getItemImage.ts +++ b/utils/getItemImage.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { ImageSource } from "expo-image"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { ImageSource } from "expo-image"; interface Props { item: BaseItemDto; diff --git a/utils/hls/parseM3U8ForSubtitles.ts b/utils/hls/parseM3U8ForSubtitles.ts index fb1902b0..ce962100 100644 --- a/utils/hls/parseM3U8ForSubtitles.ts +++ b/utils/hls/parseM3U8ForSubtitles.ts @@ -11,7 +11,7 @@ export interface SubtitleTrack { } export async function parseM3U8ForSubtitles( - url: string + url: string, ): Promise { try { const response = await axios.get(url, { responseType: "text" }); diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index 068d6058..2c32b615 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -1,10 +1,10 @@ // utils/getDefaultPlaySettings.ts import { BITRATES } from "@/components/BitrateSelector"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import { Settings, useSettings } from "../atoms/settings"; +import { type Settings, useSettings } from "../atoms/settings"; import { AudioStreamRanker, StreamRanker, @@ -34,7 +34,7 @@ export function getDefaultPlaySettings( item: BaseItemDto, settings: Settings, previousIndexes?: previousIndexes, - previousSource?: MediaSourceInfo + previousSource?: MediaSourceInfo, ): PlaySettings { if (item.Type === "Program") { return { @@ -53,14 +53,14 @@ export function getDefaultPlaySettings( // 2. Get default or preferred audio const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex; const preferedAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage + (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage, )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" + (x) => x.Type === "Audio", )?.Index; // We prefer the previous track over the default track. - let trackOptions: TrackOptions = { + const trackOptions: TrackOptions = { DefaultAudioStreamIndex: defaultAudioIndex ?? -1, DefaultSubtitleStreamIndex: mediaSource?.DefaultSubtitleStreamIndex ?? -1, }; @@ -74,7 +74,7 @@ export function getDefaultPlaySettings( previousIndexes.subtitleIndex, previousSource, mediaStreams, - trackOptions + trackOptions, ); } } @@ -87,7 +87,7 @@ export function getDefaultPlaySettings( previousIndexes.audioIndex, previousSource, mediaStreams, - trackOptions + trackOptions, ); } } diff --git a/utils/jellyfin/image/getBackdropUrl.ts b/utils/jellyfin/image/getBackdropUrl.ts index dd138aab..eb55e9d4 100644 --- a/utils/jellyfin/image/getBackdropUrl.ts +++ b/utils/jellyfin/image/getBackdropUrl.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getPrimaryImageUrl } from "./getPrimaryImageUrl"; /** diff --git a/utils/jellyfin/image/getLogoImageUrlById.ts b/utils/jellyfin/image/getLogoImageUrlById.ts index 3712b888..97ca3fbe 100644 --- a/utils/jellyfin/image/getLogoImageUrlById.ts +++ b/utils/jellyfin/image/getLogoImageUrlById.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; /** * Retrieves the primary image URL for a given item. diff --git a/utils/jellyfin/image/getParentBackdropImageUrl.ts b/utils/jellyfin/image/getParentBackdropImageUrl.ts index 4a03795b..024bb045 100644 --- a/utils/jellyfin/image/getParentBackdropImageUrl.ts +++ b/utils/jellyfin/image/getParentBackdropImageUrl.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { - BaseItemDto, + type BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { isBaseItemDto } from "../jellyfin"; diff --git a/utils/jellyfin/image/getPrimaryImageUrl.ts b/utils/jellyfin/image/getPrimaryImageUrl.ts index cd354308..18124374 100644 --- a/utils/jellyfin/image/getPrimaryImageUrl.ts +++ b/utils/jellyfin/image/getPrimaryImageUrl.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; diff --git a/utils/jellyfin/image/getPrimaryImageUrlById.ts b/utils/jellyfin/image/getPrimaryImageUrlById.ts index 736d1a0e..e9388233 100644 --- a/utils/jellyfin/image/getPrimaryImageUrlById.ts +++ b/utils/jellyfin/image/getPrimaryImageUrlById.ts @@ -1,4 +1,4 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; /** * Retrieves the primary image URL for a given item. diff --git a/utils/jellyfin/image/getPrimaryParentImageUrl.ts b/utils/jellyfin/image/getPrimaryParentImageUrl.ts index ff862624..9a51edf4 100644 --- a/utils/jellyfin/image/getPrimaryParentImageUrl.ts +++ b/utils/jellyfin/image/getPrimaryParentImageUrl.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { - BaseItemDto, + type BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { isBaseItemDto } from "../jellyfin"; diff --git a/utils/jellyfin/jellyfin.ts b/utils/jellyfin/jellyfin.ts index 80738422..3db4ba8e 100644 --- a/utils/jellyfin/jellyfin.ts +++ b/utils/jellyfin/jellyfin.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; /** * Generates the authorization headers for Jellyfin API requests. diff --git a/utils/jellyfin/media/getPlaybackUrl.ts b/utils/jellyfin/media/getPlaybackUrl.ts index 74257348..0fd23d0c 100644 --- a/utils/jellyfin/media/getPlaybackUrl.ts +++ b/utils/jellyfin/media/getPlaybackUrl.ts @@ -1,4 +1,4 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 982bb413..18f9357a 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -1,6 +1,6 @@ import native from "@/utils/profiles/native"; -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, MediaSourceInfo, PlaybackInfoResponse, @@ -63,7 +63,7 @@ export const getStreamUrl = async ({ data: { deviceProfile, }, - } + }, ); const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl; sessionId = res0.data.PlaySessionId || null; @@ -95,7 +95,7 @@ export const getStreamUrl = async ({ audioStreamIndex, subtitleStreamIndex, }, - } + }, ); if (res2.status !== 200) { @@ -105,7 +105,7 @@ export const getStreamUrl = async ({ sessionId = res2.data.PlaySessionId || null; mediaSource = res2.data.MediaSources?.find( - (source: MediaSourceInfo) => source.Id === mediaSourceId + (source: MediaSourceInfo) => source.Id === mediaSourceId, ); if (item.MediaType === "Video") { diff --git a/utils/jellyfin/playstate/markAsNotPlayed.ts b/utils/jellyfin/playstate/markAsNotPlayed.ts index c84ceb8a..1478c965 100644 --- a/utils/jellyfin/playstate/markAsNotPlayed.ts +++ b/utils/jellyfin/playstate/markAsNotPlayed.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { AxiosError } from "axios"; +import type { Api } from "@jellyfin/sdk"; +import type { AxiosError } from "axios"; interface MarkAsNotPlayedParams { api: Api | null | undefined; diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index 9b6ee13d..e17638ec 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; interface MarkAsPlayedParams { @@ -14,7 +14,11 @@ interface MarkAsPlayedParams { * @param params - The parameters for marking an item as played∏ * @returns A promise that resolves to true if the operation was successful, false otherwise */ -export const markAsPlayed = async ({ api, item, userId }: MarkAsPlayedParams): Promise => { +export const markAsPlayed = async ({ + api, + item, + userId, +}: MarkAsPlayedParams): Promise => { if (!api || !item?.Id || !userId || !item.RunTimeTicks) { console.error("Invalid parameters for markAsPlayed"); return false; diff --git a/utils/jellyfin/playstate/reportPlaybackProgress.ts b/utils/jellyfin/playstate/reportPlaybackProgress.ts index 1342690a..282a9b59 100644 --- a/utils/jellyfin/playstate/reportPlaybackProgress.ts +++ b/utils/jellyfin/playstate/reportPlaybackProgress.ts @@ -1,17 +1,17 @@ -import { Api } from "@jellyfin/sdk"; -import { getAuthHeaders } from "../jellyfin"; -import { postCapabilities } from "../session/capabilities"; -import { Settings } from "@/utils/atoms/settings"; +import { getOrSetDeviceId } from "@/providers/JellyfinProvider"; +import type { Settings } from "@/utils/atoms/settings"; +import ios from "@/utils/profiles/ios"; +import native from "@/utils/profiles/native"; +import old from "@/utils/profiles/old"; +import type { Api } from "@jellyfin/sdk"; +import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client"; import { getMediaInfoApi, getPlaystateApi, getSessionApi, } from "@jellyfin/sdk/lib/utils/api"; -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client"; -import { getOrSetDeviceId } from "@/providers/JellyfinProvider"; -import ios from "@/utils/profiles/ios"; -import native from "@/utils/profiles/native"; -import old from "@/utils/profiles/old"; +import { getAuthHeaders } from "../jellyfin"; +import { postCapabilities } from "../session/capabilities"; interface ReportPlaybackProgressParams { api?: Api | null; diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index c0f3b295..c1b53906 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,7 +1,7 @@ -import { Settings } from "@/utils/atoms/settings"; +import type { Settings } from "@/utils/atoms/settings"; import native from "@/utils/profiles/native"; -import { Api } from "@jellyfin/sdk"; -import { AxiosError, AxiosResponse } from "axios"; +import type { Api } from "@jellyfin/sdk"; +import type { AxiosError, AxiosResponse } from "axios"; import { getAuthHeaders } from "../jellyfin"; interface PostCapabilitiesParams { @@ -47,7 +47,7 @@ export const postCapabilities = async ({ }, { headers: getAuthHeaders(api), - } + }, ); return d; } catch (error: any | AxiosError) { diff --git a/utils/jellyfin/tvshows/nextUp.ts b/utils/jellyfin/tvshows/nextUp.ts index 22468b0f..dd7396d2 100644 --- a/utils/jellyfin/tvshows/nextUp.ts +++ b/utils/jellyfin/tvshows/nextUp.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { AxiosError } from "axios"; import { getAuthHeaders } from "../jellyfin"; diff --git a/utils/jellyfin/user-library/getItemById.ts b/utils/jellyfin/user-library/getItemById.ts index 79914abc..261733b6 100644 --- a/utils/jellyfin/user-library/getItemById.ts +++ b/utils/jellyfin/user-library/getItemById.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/jellyfin/user-library/getUserItemData.ts b/utils/jellyfin/user-library/getUserItemData.ts index 56b0dae0..d74db625 100644 --- a/utils/jellyfin/user-library/getUserItemData.ts +++ b/utils/jellyfin/user-library/getUserItemData.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/log.tsx b/utils/log.tsx index 45999062..3344b9ff 100644 --- a/utils/log.tsx +++ b/utils/log.tsx @@ -1,7 +1,8 @@ -import { atomWithStorage, createJSONStorage } from "jotai/utils"; -import { storage } from "./mmkv"; import { useQuery } from "@tanstack/react-query"; -import React, { createContext, useContext } from "react"; +import { atomWithStorage, createJSONStorage } from "jotai/utils"; +import type React from "react"; +import { createContext, useContext } from "react"; +import { storage } from "./mmkv"; type LogLevel = "INFO" | "WARN" | "ERROR"; @@ -20,10 +21,10 @@ const mmkvStorage = createJSONStorage(() => ({ const logsAtom = atomWithStorage("logs", [], mmkvStorage); const LogContext = createContext | null>( - null + null, ); const DownloadContext = createContext | null>( - null + null, ); function useLogProvider() { diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts index 61d17a9a..70e10419 100644 --- a/utils/optimize-server.ts +++ b/utils/optimize-server.ts @@ -1,12 +1,12 @@ import { itemRouter } from "@/components/common/TouchableItemRouter"; -import { +import { DownloadedItem } from "@/providers/DownloadProvider"; +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import axios from "axios"; -import { writeToLog } from "./log"; -import { DownloadedItem } from "@/providers/DownloadProvider"; import { MMKV } from "react-native-mmkv"; +import { writeToLog } from "./log"; interface IJobInput { deviceId?: string | null; @@ -63,7 +63,7 @@ export async function getAllJobsByDeviceId({ console.error( statusResponse.status, statusResponse.data, - statusResponse.statusText + statusResponse.statusText, ); throw new Error("Failed to fetch job status"); } @@ -172,7 +172,7 @@ export async function getStatistics({ export function saveDownloadItemInfoToDiskTmp( item: BaseItemDto, mediaSource: MediaSourceInfo, - url: string + url: string, ): boolean { try { const storage = new MMKV(); diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index 5199dfa7..3844e699 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -1,4 +1,4 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecast: DeviceProfile = { Name: "Chromecast Video Profile", diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts index e74827b2..42bb1712 100644 --- a/utils/profiles/chromecasth265.ts +++ b/utils/profiles/chromecasth265.ts @@ -1,4 +1,4 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecasth265: DeviceProfile = { Name: "Chromecast Video Profile", diff --git a/utils/store.ts b/utils/store.ts index 09a7aa5b..2ff6e1b4 100644 --- a/utils/store.ts +++ b/utils/store.ts @@ -1,3 +1,3 @@ -import { createStore } from 'jotai'; +import { createStore } from "jotai"; export const store = createStore(); diff --git a/utils/streamRanker.ts b/utils/streamRanker.ts index 665e57be..7e958435 100644 --- a/utils/streamRanker.ts +++ b/utils/streamRanker.ts @@ -1,4 +1,4 @@ -import { +import type { MediaSourceInfo, MediaStream, } from "@jellyfin/sdk/lib/generated-client"; @@ -10,14 +10,14 @@ abstract class StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void; protected rank( prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { if (prevIndex == -1) { console.debug(`AutoSet Subtitle - No Stream Set`); @@ -41,7 +41,7 @@ abstract class StreamRankerStrategy { } console.debug( - `AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}` + `AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}`, ); let prevRelIndex = 0; @@ -74,7 +74,7 @@ abstract class StreamRankerStrategy { score += 2; console.debug( - `AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}` + `AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`, ); if (score > bestStreamScore && score >= 3) { bestStreamScore = score; @@ -86,12 +86,12 @@ abstract class StreamRankerStrategy { if (bestStreamIndex != null) { console.debug( - `AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.` + `AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`, ); trackOptions[`Default${this.streamType}StreamIndex`] = bestStreamIndex; } else { console.debug( - `AutoSet ${this.streamType} - Threshold not met. Using default.` + `AutoSet ${this.streamType} - Threshold not met. Using default.`, ); } } @@ -104,7 +104,7 @@ class SubtitleStreamRanker extends StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { super.rank(prevIndex, prevSource, mediaStreams, trackOptions); } @@ -117,7 +117,7 @@ class AudioStreamRanker extends StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { super.rank(prevIndex, prevSource, mediaStreams, trackOptions); } @@ -138,7 +138,7 @@ class StreamRanker { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ) { this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions); } diff --git a/utils/textTools.ts b/utils/textTools.ts index ce12b61b..9472765c 100644 --- a/utils/textTools.ts +++ b/utils/textTools.ts @@ -1,7 +1,7 @@ /* * Truncate a text longer than a certain length */ -export const tc = (text: string | null | undefined, length: number = 20) => { +export const tc = (text: string | null | undefined, length = 20) => { if (!text) return ""; return text.length > length ? text.substr(0, length) + "..." : text; }; diff --git a/utils/time.ts b/utils/time.ts index 24baf21f..76e02077 100644 --- a/utils/time.ts +++ b/utils/time.ts @@ -6,7 +6,7 @@ * @returns A string formatted as "Xh Ym" where X is hours and Y is minutes. */ export const runtimeTicksToMinutes = ( - ticks: number | null | undefined + ticks: number | null | undefined, ): string => { if (!ticks) return "0h 0m"; @@ -21,7 +21,7 @@ export const runtimeTicksToMinutes = ( }; export const runtimeTicksToSeconds = ( - ticks: number | null | undefined + ticks: number | null | undefined, ): string => { if (!ticks) return "0h 0m"; @@ -39,7 +39,7 @@ export const runtimeTicksToSeconds = ( // t: ms export const formatTimeString = ( t: number | null | undefined, - unit: "s" | "ms" | "tick" = "ms" + unit: "s" | "ms" | "tick" = "ms", ): string => { if (t === null || t === undefined) return "0:00"; diff --git a/utils/useReactNavigationQuery.ts b/utils/useReactNavigationQuery.ts index a0c5b307..1cbe40e8 100644 --- a/utils/useReactNavigationQuery.ts +++ b/utils/useReactNavigationQuery.ts @@ -1,9 +1,9 @@ import { useFocusEffect } from "@react-navigation/core"; import { - QueryKey, + type QueryKey, + type UseQueryOptions, + type UseQueryResult, useQuery, - UseQueryOptions, - UseQueryResult, } from "@tanstack/react-query"; import { useCallback } from "react"; @@ -11,9 +11,9 @@ export function useReactNavigationQuery< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey + TQueryKey extends QueryKey = QueryKey, >( - options: UseQueryOptions + options: UseQueryOptions, ): UseQueryResult { const useQueryReturn = useQuery(options); @@ -25,7 +25,7 @@ export function useReactNavigationQuery< options.enabled !== false ) useQueryReturn.refetch(); - }, [options.enabled, options.refetchOnWindowFocus]) + }, [options.enabled, options.refetchOnWindowFocus]), ); return useQueryReturn; From 5757b1c010e423f2f721ef5229430ecfb390cf10 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 16 Mar 2025 18:08:55 +0100 Subject: [PATCH 84/93] fix: lint --- bun.lock | 144 +++++++++++++++++++++---- i18n.ts | 2 +- modules/VlcPlayer.types.ts | 2 +- utils/jellyfin/session/capabilities.ts | 6 +- 4 files changed, 126 insertions(+), 28 deletions(-) diff --git a/bun.lock b/bun.lock index e00dab00..c2fdfe7d 100644 --- a/bun.lock +++ b/bun.lock @@ -107,6 +107,8 @@ "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.0.0", "@types/uuid": "^10.0.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.0", "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", @@ -781,7 +783,7 @@ "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "ansi-fragments": ["ansi-fragments@0.2.1", "", { "dependencies": { "colorette": "^1.0.7", "slice-ansi": "^2.0.0", "strip-ansi": "^5.0.0" } }, "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w=="], @@ -939,6 +941,8 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -955,7 +959,7 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -1091,6 +1095,8 @@ "envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "eol": ["eol@0.9.1", "", {}, "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], @@ -1121,6 +1127,8 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], @@ -1289,6 +1297,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1343,6 +1353,8 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], "i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="], @@ -1529,10 +1541,14 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw=="], - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lint-staged": ["lint-staged@15.5.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg=="], + + "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], + "load-bmfont": ["load-bmfont@1.4.2", "", { "dependencies": { "buffer-equal": "0.0.1", "mime": "^1.3.4", "parse-bmfont-ascii": "^1.0.3", "parse-bmfont-binary": "^1.0.5", "parse-bmfont-xml": "^1.1.4", "phin": "^3.7.1", "xhr": "^2.0.1", "xtend": "^4.0.0" } }, "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -1545,6 +1561,8 @@ "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "logkitty": ["logkitty@0.7.1", "", { "dependencies": { "ansi-fragments": "^0.2.1", "dayjs": "^1.8.15", "yargs": "^15.1.0" }, "bin": { "logkitty": "bin/logkitty.js" } }, "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1609,6 +1627,8 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "min-document": ["min-document@2.19.0", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1753,7 +1773,9 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -1971,6 +1993,8 @@ "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], @@ -2039,7 +2063,7 @@ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], - "slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], @@ -2073,6 +2097,8 @@ "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2249,7 +2275,7 @@ "wonka": ["wonka@6.3.4", "", {}, "sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2313,8 +2339,12 @@ "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + "@expo/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@expo/cli/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "@expo/cli/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], @@ -2431,9 +2461,9 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "ansi-fragments/slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -2449,8 +2479,12 @@ "chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "compressible/mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2505,8 +2539,6 @@ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jscodeshift/tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], @@ -2515,10 +2547,22 @@ "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "lint-staged/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "lint-staged/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "load-bmfont/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "load-bmfont/phin": ["phin@3.7.1", "", { "dependencies": { "centra": "^2.7.0" } }, "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ=="], + "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "make-dir/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -2535,8 +2579,6 @@ "metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2557,6 +2599,8 @@ "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + "password-prompt/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "patch-package/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "patch-package/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], @@ -2571,8 +2615,6 @@ "postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -2609,8 +2651,6 @@ "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], @@ -2629,9 +2669,9 @@ "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - "slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2647,6 +2687,8 @@ "tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "tailwindcss/postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], "tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], @@ -2661,6 +2703,8 @@ "tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + "terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2671,7 +2715,11 @@ "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2693,6 +2741,8 @@ "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], + "@expo/cli/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "@expo/image-utils/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], @@ -2751,8 +2801,16 @@ "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.77.0", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.77.0" } }, "sha512-5TYPn1k+jdDOZJU4EVb1kZ0p9TCVICXK3uplRev5Gul57oWesAaiWGZOzfRS3lonWeuR4ij8v8PFfIHOaq0vmA=="], + "ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "ansi-fragments/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + "chromium-edge-launcher/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -2777,6 +2835,28 @@ "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "lint-staged/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "lint-staged/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "lint-staged/execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "lint-staged/execa/npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "lint-staged/execa/onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "lint-staged/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "lint-staged/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "logkitty/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -2803,6 +2883,8 @@ "parse-bmfont-xml/xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "password-prompt/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "patch-package/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], "patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -2819,14 +2901,18 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "terminal-link/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -2861,8 +2947,12 @@ "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "chromium-edge-launcher/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "default-gateway/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], "default-gateway/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -2875,6 +2965,14 @@ "del/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "lint-staged/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "lint-staged/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "logkitty/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2897,8 +2995,6 @@ "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "temp/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -2923,6 +3019,8 @@ "@react-native/babel-plugin-codegen/@react-native/codegen/jscodeshift/recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "chromium-edge-launcher/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "default-gateway/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], diff --git a/i18n.ts b/i18n.ts index 47246a59..040e1a4f 100644 --- a/i18n.ts +++ b/i18n.ts @@ -25,7 +25,7 @@ export const APP_LANGUAGES = [ { label: "日本語", value: "ja" }, { label: "Türkçe", value: "tr" }, { label: "Nederlands", value: "nl" }, - { label: 'Polski', value: 'pl' }, + { label: "Polski", value: "pl" }, { label: "Svenska", value: "sv" }, { label: "Українська", value: "ua" }, { label: "简体中文", value: "zh-CN" }, diff --git a/modules/VlcPlayer.types.ts b/modules/VlcPlayer.types.ts index e1c37797..128ac60c 100644 --- a/modules/VlcPlayer.types.ts +++ b/modules/VlcPlayer.types.ts @@ -59,7 +59,7 @@ export type ChapterInfo = { export type VlcPlayerViewProps = { source: VlcPlayerSource; - style?: Object; + style?: Record; progressUpdateInterval?: number; paused?: boolean; muted?: boolean; diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index c1b53906..1c2a04af 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,7 +1,7 @@ import type { Settings } from "@/utils/atoms/settings"; import native from "@/utils/profiles/native"; import type { Api } from "@jellyfin/sdk"; -import type { AxiosError, AxiosResponse } from "axios"; +import type { AxiosResponse } from "axios"; import { getAuthHeaders } from "../jellyfin"; interface PostCapabilitiesParams { @@ -28,7 +28,7 @@ export const postCapabilities = async ({ try { const d = api.axiosInstance.post( - api.basePath + "/Sessions/Capabilities/Full", + `${api.basePath}/Sessions/Capabilities/Full`, { playableMediaTypes: ["Audio", "Video"], supportedCommands: [ @@ -50,7 +50,7 @@ export const postCapabilities = async ({ }, ); return d; - } catch (error: any | AxiosError) { + } catch (error) { throw new Error("Failed to mark as not played"); } }; From f7e771123f6ddc54e285b5b5e0ad53a7f611fe45 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 16 Mar 2025 18:09:53 +0100 Subject: [PATCH 85/93] fix: lint config --- biome.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/biome.json b/biome.json index fea15f82..f7db0c44 100644 --- a/biome.json +++ b/biome.json @@ -12,10 +12,6 @@ "noNonNullAssertion": "off" }, "recommended": true, - "style": { - "useImportType": "off", - "noNonNullAssertion": "off" - }, "correctness": { "useExhaustiveDependencies": "off" }, "suspicious": { "noExplicitAny": "off" From f770cf174b8888f30c6aa4f30f561339fb8010b7 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 16 Mar 2025 18:12:10 +0100 Subject: [PATCH 86/93] fix: linting config --- biome.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index f7db0c44..bf2ae1c8 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,15 @@ "organizeImports": { "enabled": true }, - "files": {}, + "files": { + "ignore": [ + "node_modules", + "ios", + "android", + "Streamyfin.app", + "utils/jellyseerr" + ] + }, "linter": { "enabled": true, "rules": { From 2189b3d3dd021cbfa50a2188fa793376b40f54a6 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 16 Mar 2025 21:31:33 +0100 Subject: [PATCH 87/93] fix: android push notifications --- .gitignore | 2 ++ app.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 00eb8098..0944bda6 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ credentials.json .idea/ .ruby-lsp modules/hls-downloader/android/build +streamyfin-4fec1-firebase-adminsdk.json +google-services.json \ No newline at end of file diff --git a/app.json b/app.json index 0a421f90..1ebd0526 100644 --- a/app.json +++ b/app.json @@ -41,7 +41,8 @@ "android.permission.FOREGROUND_SERVICE", "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK", "android.permission.WRITE_SETTINGS" - ] + ], + "googleServicesFile": "./google-services.json" }, "plugins": [ "@react-native-tvos/config-tv", From 8e8ae32287818aa772c2dc4b3f2d3f6e95b32105 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 17 Mar 2025 09:45:51 +0100 Subject: [PATCH 88/93] fix: remove patch --- package.json | 2 -- ...@expo+react-native-action-sheet+4.1.0.patch | 18 ------------------ 2 files changed, 20 deletions(-) delete mode 100644 patches/@expo+react-native-action-sheet+4.1.0.patch diff --git a/package.json b/package.json index 5df34655..c8592aff 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", "prepare": "husky", - "postinstall": "patch-package", "lint": "biome format --write ." }, "dependencies": { @@ -122,7 +121,6 @@ "@types/uuid": "^10.0.0", "husky": "^9.1.7", "lint-staged": "^15.5.0", - "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", "typescript": "~5.7.3" diff --git a/patches/@expo+react-native-action-sheet+4.1.0.patch b/patches/@expo+react-native-action-sheet+4.1.0.patch deleted file mode 100644 index c2640492..00000000 --- a/patches/@expo+react-native-action-sheet+4.1.0.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js b/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -index 2a6943f..42d40e0 100644 ---- a/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -+++ b/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -@@ -1,2 +1,2 @@ --var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");var _interopRequireWildcard=require("@babel/runtime/helpers/interopRequireWildcard");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _classCallCheck2=_interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));var _createClass2=_interopRequireDefault(require("@babel/runtime/helpers/createClass"));var _assertThisInitialized2=_interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));var _inherits2=_interopRequireDefault(require("@babel/runtime/helpers/inherits"));var _possibleConstructorReturn2=_interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));var _getPrototypeOf2=_interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));var React=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _ActionGroup=_interopRequireDefault(require("./ActionGroup"));var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=(0,_getPrototypeOf2.default)(Derived),result;if(hasNativeReflectConstruct){var NewTarget=(0,_getPrototypeOf2.default)(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return(0,_possibleConstructorReturn2.default)(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=_reactNative.Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=_reactNative.Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){(0,_inherits2.default)(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;(0,_classCallCheck2.default)(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new _reactNative.Animated.Value(0),sheetOpacity:new _reactNative.Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind((0,_assertThisInitialized2.default)(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_reactNative.BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind((0,_assertThisInitialized2.default)(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}_reactNative.BackHandler.removeEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);_this.setState({isAnimating:true});_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}(0,_createClass2.default)(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(_reactNative.Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(_reactNative.Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(_reactNative.Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(_reactNative.View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(_reactNative.View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(_reactNative.Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(_reactNative.TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(_reactNative.Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(_reactNative.View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(_ActionGroup.default,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);exports.default=CustomActionSheet;var styles=_reactNative.StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); -+var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");var _interopRequireWildcard=require("@babel/runtime/helpers/interopRequireWildcard");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _classCallCheck2=_interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));var _createClass2=_interopRequireDefault(require("@babel/runtime/helpers/createClass"));var _assertThisInitialized2=_interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));var _inherits2=_interopRequireDefault(require("@babel/runtime/helpers/inherits"));var _possibleConstructorReturn2=_interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));var _getPrototypeOf2=_interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));var React=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _ActionGroup=_interopRequireDefault(require("./ActionGroup"));var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=(0,_getPrototypeOf2.default)(Derived),result;if(hasNativeReflectConstruct){var NewTarget=(0,_getPrototypeOf2.default)(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return(0,_possibleConstructorReturn2.default)(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=_reactNative.Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=_reactNative.Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){(0,_inherits2.default)(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;(0,_classCallCheck2.default)(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this._backHandlerListener=null;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new _reactNative.Animated.Value(0),sheetOpacity:new _reactNative.Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind((0,_assertThisInitialized2.default)(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_this._backHandlerListener=_reactNative.BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind((0,_assertThisInitialized2.default)(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}if(_this._backHandlerListener){_this._backHandlerListener.remove();};_this.setState({isAnimating:true});_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}(0,_createClass2.default)(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(_reactNative.Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(_reactNative.Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(_reactNative.Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(_reactNative.View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(_reactNative.View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(_reactNative.Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(_reactNative.TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(_reactNative.Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(_reactNative.View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(_ActionGroup.default,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);exports.default=CustomActionSheet;var styles=_reactNative.StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); - //# sourceMappingURL=CustomActionSheet.js.map -\ No newline at end of file -diff --git a/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js b/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -index 253c851..2eb2ba2 100644 ---- a/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -+++ b/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -@@ -1,2 +1,2 @@ --import _classCallCheck from"@babel/runtime/helpers/classCallCheck";import _createClass from"@babel/runtime/helpers/createClass";import _assertThisInitialized from"@babel/runtime/helpers/assertThisInitialized";import _inherits from"@babel/runtime/helpers/inherits";import _possibleConstructorReturn from"@babel/runtime/helpers/possibleConstructorReturn";import _getPrototypeOf from"@babel/runtime/helpers/getPrototypeOf";var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=_getPrototypeOf(Derived),result;if(hasNativeReflectConstruct){var NewTarget=_getPrototypeOf(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return _possibleConstructorReturn(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}import*as React from'react';import{Animated,BackHandler,Easing,Modal,Platform,StyleSheet,TouchableWithoutFeedback,View}from'react-native';import ActionGroup from'./ActionGroup';var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){_inherits(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;_classCallCheck(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new Animated.Value(0),sheetOpacity:new Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind(_assertThisInitialized(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);Animated.parallel([Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind(_assertThisInitialized(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}BackHandler.removeEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);_this.setState({isAnimating:true});Animated.parallel([Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}_createClass(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(ActionGroup,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);export{CustomActionSheet as default};var styles=StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); -+import _classCallCheck from"@babel/runtime/helpers/classCallCheck";import _createClass from"@babel/runtime/helpers/createClass";import _assertThisInitialized from"@babel/runtime/helpers/assertThisInitialized";import _inherits from"@babel/runtime/helpers/inherits";import _possibleConstructorReturn from"@babel/runtime/helpers/possibleConstructorReturn";import _getPrototypeOf from"@babel/runtime/helpers/getPrototypeOf";var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=_getPrototypeOf(Derived),result;if(hasNativeReflectConstruct){var NewTarget=_getPrototypeOf(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return _possibleConstructorReturn(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}import*as React from'react';import{Animated,BackHandler,Easing,Modal,Platform,StyleSheet,TouchableWithoutFeedback,View}from'react-native';import ActionGroup from'./ActionGroup';var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){_inherits(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;_classCallCheck(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this._backHandlerListener=null;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new Animated.Value(0),sheetOpacity:new Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind(_assertThisInitialized(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);Animated.parallel([Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_this._backHandlerListener=BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind(_assertThisInitialized(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}if(_this._backHandlerListener){_this._backHandlerListener.remove();};_this.setState({isAnimating:true});Animated.parallel([Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}_createClass(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(ActionGroup,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);export{CustomActionSheet as default};var styles=StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); - //# sourceMappingURL=CustomActionSheet.js.map -\ No newline at end of file \ No newline at end of file From 2932a7b3240fa3b76ed6751aa53f7f9d348f1ec2 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 17 Mar 2025 09:56:18 +0100 Subject: [PATCH 89/93] chore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0944bda6..b00b59ca 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ credentials.json .ruby-lsp modules/hls-downloader/android/build streamyfin-4fec1-firebase-adminsdk.json -google-services.json \ No newline at end of file +google-services.json +.env \ No newline at end of file From 0b22f28bb66f02cd52a9c5baac68167bf7c46ec1 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 17 Mar 2025 09:56:24 +0100 Subject: [PATCH 90/93] fix: env --- app.config.js | 5 ++++- eas.json | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app.config.js b/app.config.js index 30ae0d5b..b67ee80f 100644 --- a/app.config.js +++ b/app.config.js @@ -1,11 +1,14 @@ module.exports = ({ config }) => { - if (process.env.EXPO_TV != "1") { + if (process.env.EXPO_TV !== "1") { config.plugins.push([ "react-native-google-cast", { useDefaultExpandedMediaControls: true }, ]); } return { + android: { + googleServicesFile: process.env.GOOGLE_SERVICES_JSON, + }, ...config, }; }; diff --git a/eas.json b/eas.json index 0df93411..18d8aa68 100644 --- a/eas.json +++ b/eas.json @@ -34,7 +34,11 @@ "production": { "channel": "0.27.0", "android": { - "image": "latest" + "image": "latest", + "buildType": "app-bundle" + }, + "env": { + "GOOGLE_SERVICES_JSON": "GOOGLE_SERVICES_JSON" } }, "production-apk": { @@ -42,6 +46,9 @@ "android": { "buildType": "apk", "image": "latest" + }, + "env": { + "GOOGLE_SERVICES_JSON": "GOOGLE_SERVICES_JSON" } }, "production-apk-tv": { From b9c02618d5df4b83dbf82916b565f520a1a070a6 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 17 Mar 2025 10:11:22 +0100 Subject: [PATCH 91/93] fix: intentionally commit the google-services file - not a secret --- .gitignore | 1 - google-services.json | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 google-services.json diff --git a/.gitignore b/.gitignore index b00b59ca..6dc107d8 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,4 @@ credentials.json .ruby-lsp modules/hls-downloader/android/build streamyfin-4fec1-firebase-adminsdk.json -google-services.json .env \ No newline at end of file diff --git a/google-services.json b/google-services.json new file mode 100644 index 00000000..6901246b --- /dev/null +++ b/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1039325086281", + "project_id": "streamyfin-4fec1", + "storage_bucket": "streamyfin-4fec1.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1039325086281:android:b270168004aa30c1e1f598", + "android_client_info": { + "package_name": "com.fredrikburmester.streamyfin" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyABKBTRL9dvGuZbx009YFLUAPurFIDovFQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} From 8ab72b1262c33e6c7df73dc0cdb5a55e563eb6e8 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 17 Mar 2025 10:11:25 +0100 Subject: [PATCH 92/93] chore --- eas.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/eas.json b/eas.json index 18d8aa68..0df93411 100644 --- a/eas.json +++ b/eas.json @@ -34,11 +34,7 @@ "production": { "channel": "0.27.0", "android": { - "image": "latest", - "buildType": "app-bundle" - }, - "env": { - "GOOGLE_SERVICES_JSON": "GOOGLE_SERVICES_JSON" + "image": "latest" } }, "production-apk": { @@ -46,9 +42,6 @@ "android": { "buildType": "apk", "image": "latest" - }, - "env": { - "GOOGLE_SERVICES_JSON": "GOOGLE_SERVICES_JSON" } }, "production-apk-tv": { From f1a3b480175294b6b4a66d9f33a30f0d2f312ccf Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 19 Mar 2025 11:33:27 +0100 Subject: [PATCH 93/93] chore --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 1ebd0526..8f10deb3 100644 --- a/app.json +++ b/app.json @@ -31,7 +31,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 53, + "versionCode": 54, "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive_icon.png", "backgroundColor": "#464646"