From 21a82789a70d50aba83586992995cfa6b474859c Mon Sep 17 00:00:00 2001 From: iris1598 Date: Fri, 30 Aug 2024 21:41:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prbuild.yml | 30 +- .github/workflows/release.yml | 2 +- assets/i18n/hu.json | 324 ----- assets/i18n/id.json | 307 ----- assets/i18n/pl.json | 268 ---- .../services/extension_jscore_plugin.dart | 136 -- .../pages/watch/video/video_player_cast.dart | 77 -- .../video/video_player_desktop_controls.dart | 1183 ----------------- .../video/video_player_mobile_controls.dart | 843 ------------ .../watch/video/video_player_sidebar.dart | 940 ------------- 10 files changed, 4 insertions(+), 4106 deletions(-) delete mode 100644 assets/i18n/hu.json delete mode 100644 assets/i18n/id.json delete mode 100644 assets/i18n/pl.json delete mode 100644 lib/data/services/extension_jscore_plugin.dart delete mode 100644 lib/views/pages/watch/video/video_player_cast.dart delete mode 100644 lib/views/pages/watch/video/video_player_desktop_controls.dart delete mode 100644 lib/views/pages/watch/video/video_player_mobile_controls.dart delete mode 100644 lib/views/pages/watch/video/video_player_sidebar.dart diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml index 2eef2b35..ff904cd5 100644 --- a/.github/workflows/prbuild.yml +++ b/.github/workflows/prbuild.yml @@ -23,7 +23,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v2 with: - flutter-version: 3.16.8 + flutter-version: 3.16.8 channel: stable - name: Decode keystore run: | @@ -62,9 +62,9 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: subosito/flutter-action@v2.12.0 + - uses: subosito/flutter-action@v2 with: - flutter-version: 3.16.8 + flutter-version: 3.16.8 channel: stable - name: Install project dependencies run: flutter pub get @@ -79,27 +79,3 @@ jobs: with: path: "build/windows/x64/runner/Miru-App" name: Miru-pr-${{ github.event.pull_request.number }}-windows.zip - - build-and-release-linux: - runs-on: ubuntu-latest - steps: - - name: Install Dependencies - run: sudo apt-get install ninja-build build-essential libgtk-3-dev libmpv-dev mpv - - uses: actions/checkout@v3 - - uses: subosito/flutter-action@v2.12.0 - with: - flutter-version: 3.16.8 - channel: stable - - name: Install project dependencies - run: flutter pub get - - name: Build artifacts - run: flutter build linux --release - - name: Rename Release Directory Name to Miru-App # 为了解压缩后更好看一点 - run: | - mv build/linux/x64/release/bundle build/linux/x64/release/Miru-App - # 发布安装包 - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - path: "build/linux/x64/release/Miru-App" - name: Miru-pr-${{ github.event.pull_request.number }}-linux.zip \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 822596df..02e18f96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,7 +123,7 @@ jobs: - uses: actions/checkout@v3 with: ref: main - - uses: subosito/flutter-action@v2.12.0 + - uses: subosito/flutter-action@v2 with: flutter-version: 3.16.8 channel: stable diff --git a/assets/i18n/hu.json b/assets/i18n/hu.json deleted file mode 100644 index 4cfd9d4f..00000000 --- a/assets/i18n/hu.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "languages": { - "be": "Беларуская", - "en": "English", - "es": "Español", - "fr": "Français", - "hu": "Magyar", - "hi": "हिंदी", - "id": "Indonesia", - "ja": "日本語", - "pl": "Polski", - "ru": "Русский", - "ryu": "うちなーぐち", - "uk": "Українська", - "zh": "中文", - "zhHant": "繁體中文" - }, - "common": { - "anime": "Anime", - "manga": "Manga", - "home": "Home", - "search": "Kereső", - "extension": "Bővítmények", - "extension-repo": "Bővítmény Repository", - "settings": "Beállítások", - "no-extension": "Nincs letöltve bővítmény", - "no-result": "Nincs releváns találat", - "no-more-data": "Nincs több adat", - "cancel": "Mégse", - "confirm": "Jóváhagyás", - "close": "Bezárás", - "copied": "Vágólapra másolva", - "uninstall": "Eltávolítás", - "install": "Letöltés", - "repo": "Repository", - "unset": "Nincs beállítva", - "extension-missing": "{package} bővítménye hiányzik", - "error": "Hiba", - "retry": "Újra", - "next": "Következő", - "previous": "Előző", - "show-all": "Összes megjelenítése", - "delete": "Törlés", - "delete-all": "Összes törlése", - "save": "Mentés", - "save-success": "Sikeres mentés", - "logout": "Kijelentkezés", - "login": "Bejelentkezés", - "no-data": "Nincs adat", - "clear": "Tisztítás", - "export": "Exportálás", - "off": "Ki", - "error-message": "Hibaüzenet", - "disconnect": "Lecsatlakozás" - }, - "home": { - "continue-watching": "Folytatás", - "favorite": "Kedvenc", - "no-record": "Nincsenek kedvencek", - "watched": "{ep} megtekintve", - "favorite-all": "Az összes {type} kedvenced" - }, - "search": { - "hint-text": "Használjd okosan a keresést!~", - "all": "Összes", - "filter": "Szűrő" - }, - "extension": { - "import": { - "title": "Bővítmény Importálása", - "url-label": "Bővítmény URL", - "tips": "Tudsz importálni bővítményeket linken kereszül, vagy nyomj rá a Lokális Importálásra és válaszd ki a bővítmény fájlát.", - "extension-dir": "Bővítmény Könyvtár", - "import-by-url": "URL Importálás", - "import-by-local": "Lokális Importálás" - }, - "error-dialog": "Hibaüzenet", - "installed": "Letöltve", - "edit-code": "Kód Szerkesztése" - }, - "extension-repo": { - "error": "Hiba lépett fel!", - "error-tips": "Kérlek, ellenőrizd a hálózati kapcsolatodat vagy a repository linkjét", - "empty": "Repository üres", - "upgrade": "Frissítés" - }, - "settings": { - "general": "Általános", - "general-subtitle": "TMDB, Nyelv, Téma, frissítés keresése...", - "extension": "Bővítmények", - "extension-subtitle": "Bővítmény repository", - "video-player": "Video Lejátszó", - "video-player-subtitle": "BT Szerver, Külső Lejátszó...", - "comic-reader": "Képregényolvasó", - "comic-reader-subtitle": "Alapértelmezett olvasó mód...", - "tracking": "Trackelés", - "tracking-subtitle": "AniList...", - "auto-tracking": "Auto Trackelés", - "auto-tracking-subtitle": "Haladás automatikus szinkronizálása, amikor befejezte a nézést \/ olvasást", - "about": "Rólunk", - "links": "Linkek", - "contributors": "Közreműködők", - "repo-url": "Bővítmény Repository URL", - "repo-url-subtitle": "Bővítmény repository URLje", - "tmdb-key": "TMDB API Kulcs", - "tmdb-key-subtitle": "TMDB API Kulcsa", - "bt-server": "BT Szerver", - "bt-server-subtitle": "BT Szerver torrentek online lejátszásához szükséges eszköz", - "bt-server-manager": "Kezelés", - "upgrade": "Szoftverfrissítés", - "upgrade-subtitle": "verzió: {version}", - "upgrade-training": "Keresés", - "auto-check-update": "Frissítések automatikus keresése", - "auto-check-update-subtitle": "Minden indításkor ellenőrizze a frissítéseket", - "language": "Nyelv", - "theme": "Téma", - "theme-subtitle": "Szoftver témájának változtatása", - "theme-system": "Rendszer", - "theme-light": "Világos", - "theme-dark": "Sötét", - "theme-black": "Fekete", - "nsfw": "NSFW", - "nsfw-subtitle": "NSFW tartalom megjelenítése", - "external-player": "Előnyben részesített videolejátszó", - "external-player-subtitle": "Jelenleg a preferált videolejátszó: {player}", - "external-player-builtin": "Beépített", - "language-subtitle": "Szoftver nyelvének változtatása", - "extension-log": "Bővítmény Log Ablak", - "extension-log-subtitle": "A bővítmények hibakeresésére szolgál", - "skip-interval": "Intervallum Kihagyása", - "skip-interval-subtitle": "Intervallum kihagyása a beépített videolejátszóhoz", - "default-reader-mode": "Alapértelmezett olvasó mód", - "network": "Hálózat", - "network-subtitle": "Proxy, User-Agent...", - "network-ua": "Webview User-Agent", - "network-ua-subtitle": "User-Agent változtatása a request headerjében a Webviewhoz és bővítményekhez.", - "proxy-type": "Proxy Típusa", - "proxy-type-subtitle": "Proxy típusa requestekhez", - "proxy-type-direct": "Direct", - "proxy-type-socks4": "Socks4", - "proxy-type-socks5": "Socks5", - "proxy-type-http": "HTTP", - "proxy": "Proxy", - "proxy-subtitle": "Proxy címe (pl. felhasználónév:jelszó@hoszt:port)", - "log": "Log", - "log-subtitle": "Naplófájl mentése, napló exportálása...", - "save-log": "Naplófájl mentése", - "save-log-subtitle": "A naplófájl automatikus mentése", - "export-log": "Napló exportálása", - "export-log-subtitle": "Naplófájl exportálása", - "advanced": "Haladó" - }, - "external-player-launching": "{player} indítása", - "detail": { - "tracking": "Trackelés", - "favorite": "Kedvenc", - "favorited": "Kedvenc", - "continue-watching": "{episode} Folytatása", - "total-episodes": "Összesen {total}", - "overview": "Áttekintés", - "cast": "Szereplők", - "additional-info": "További információ", - "get-lastest-data-error": "Nem sikerült lekérni a legfrissebb adatokat", - "modify-tmdb-binding": "TMDB Bind modosítása", - "no-tmdb-data": "Egyetlen TMDB adat sem egyezik, kérlek manuálisan bindold be", - "tmdb-key-missing": "TMDB API kulcs hiányzik, kérlek, add meg a beállításokban", - "tracker": "Trackelés" - }, - "video": { - "episodes": "Epizódok", - "watch-now": "Lejátszás", - "no-episodes": "Nincsenek epizódok", - "play-complete": "Lejátszás befejeződött", - "resume-last-playback": "Utolsó lejátszás folytatása", - "subtitle-none": "Nincs Felirat", - "subtitle": "Felirat", - "subtitle-change": "Felirat módosítása: {title}", - "subtitle-file": "Feliratfájl hozzáadása", - "torrent-downloading": "Torrent letöltés", - "no-qualities": "Minőség nem érhető el ", - "audio": "Hang", - "getting-streamlink": "Streamlink lekérése...", - "streamlink-error": "Nem sikerült megszerezni a streamlinket", - "tooltip": { - "close": "Bezárás", - "subtitle": "Felirat", - "play-list": "Lejátszási lista", - "quality": "Minőség", - "speed": "Sebesség", - "play": "Lejátszás", - "play-or-pause": "Szünet", - "previous": "Előző", - "next": "Következő", - "full-screen": "Teljes Képernyő", - "volume": "Hangerő", - "torrent-file-list": "Torrent fájlok listája" - }, - "cast": "Kiterjesztés TV-re", - "cast-device": "lejátszás itt: {device}", - "sidebar": { - "tab": { - "episodes": "Epizódok", - "qualitys": "Minőségek", - "torrentFiles": "Torrent Fájlok", - "tracks": "Trackek", - "settings": "Beállítások" - }, - "subtitle": { - "title": "Felirat", - "font-size": "Betűméret", - "font-color": "Betű szín", - "background-color": "Háttérszín", - "background-opacity": "Háttér átláthatósága", - "text-align": "Szöveg igazítás", - "font-weight": "Betűsúly", - "font-weight-normal": "Normál", - "font-weight-bold": "Félkövér" - }, - "play-mode": { - "title": "Lejátszási mód", - "loop": "Loop", - "single": "Egyetlen", - "auto-next": "Következő automatikusan" - } - } - }, - "comic-settings": { - "read-mode": "Olvasási mód", - "standard": "Alapértelmezett", - "right-to-left": "Jobbról balra", - "web-tonn": "Webtoon" - }, - "novel-settings": { - "font-size": "Betűméret" - }, - "bugreport": { - "auto-remove-subtitle": "~ napon belül töröljük", - "show-report-dialog": "Report Panel Megjelenítése", - "show-report-dialog-subtitle": "Hiba report panel megjelenítése app indításakor" - }, - "reader": { - "chapters": "Fejezetek", - "read-now": "Olvasd Most", - "no-chapters": "Nincsenek fejezetek" - }, - "upgrade": { - "check-update": "Frissítések Keresése", - "new-version": "Új verzió: {version}", - "download": "Frissítés", - "no-update": "Naprakész verzió", - "not-now": "Nem most", - "error": "Nem sikerült a frissítések keresése, hálózati hiba történt" - }, - "extension-install-error": "Nem sikerült telepíteni a bővítményt", - "extension-type": { - "video": "Video", - "novel": "Novel", - "comic": "Képregény" - }, - "extension-info": { - "author": "Szerző", - "description": "Leírás", - "version": "Verzió", - "language": "Nyelv", - "original-site": "Eredeti Weboldal", - "other-information": "További Információ", - "license": "Licenc", - "title": "Bővítmény Info" - }, - "cookie-clean": { - "title": "Cookiek tisztítása", - "subtitle": "Lehetséges, hogy a tisztítás után újra be kell jelentkezned", - "success": "Sikeres tisztítás", - "clean": "Tisztítás" - }, - "tmdb": { - "backdrops": "Hátterek", - "status": "Állapot", - "original-title": "Eredeti Cím", - "release-date": "Kiadási dátum", - "genres": "Műfajok", - "runtime": "Futásidő", - "languages": "Nyelvek" - }, - "bt-server": { - "not-installed": "BT-Szerver nincs letöltve", - "running": "BT-Szerver fut", - "stopped": "BT-Szerver leállítva", - "version": "Verzió: {version}", - "remote-version": "Távoli Verzió: {version}", - "stop": "Leállítás", - "upgrade": "Frissítés", - "start": "Indítás" - }, - "report": { - "copied": "Vágólapra másolva", - "github-bug-report": "Problémát GitHub issueként jelentsd", - "title": "Hibajelentés", - "copy-message": "Hibaüzenet másolása", - "show-report-checkbox": "Report panel megjelenítése app indításakor" - }, - "anilist": { - "title": "AniList Trackelés", - "login-hint-1": "Úgylátszik, hogy nem vagy bejelentkezve AniListbe", - "login-hint-2": "Elősszőr jelentkezz be az AniListbe", - "status": "Státusz", - "score": "Vélemény", - "watching": "Nézve", - "reading": "Olvasva", - "completed": "Befejezve", - "paused": "Megállítva", - "dropped": "Eldobva", - "planning": "Tervezve", - "hold-on": "Szünetelve", - "re-watching": "Újra nézve", - "re-reading": "Újra olvasva", - "start-date": "Kezdő Dátum", - "end-date": "Befejezés Dátum", - "unbind": "Unbind", - "episodes": "Epizódok", - "manga-chapter-read": "Manga Fejezetek Elolvasva: {chapters}", - "anime-episode-watch": "Anime Epizódok Megnézve: {episodes}" - } -} diff --git a/assets/i18n/id.json b/assets/i18n/id.json deleted file mode 100644 index ce4c07d2..00000000 --- a/assets/i18n/id.json +++ /dev/null @@ -1,307 +0,0 @@ -{ - "common": { - "anime": "Anime", - "manga": "Manga", - "home": "Beranda", - "search": "Cari", - "extension": "Ekstensi", - "extension-repo": "Repository Ekstensi", - "settings": "Pengaturan", - "no-extension": "Tidak ada ekstensi", - "no-result": "Tidak ada hasil", - "no-more-data": "Tidak ada data", - "cancel": "Batal", - "confirm": "Konfirmasi", - "close": "Tutup", - "copied": "Tersalin", - "uninstall": "Uninstall", - "install": "Install", - "repo": "Repository", - "unset": "Tidak diatur", - "extension-missing": "Ekstensi {package} tidak ditemukan", - "error": "Kesalahan", - "retry": "Mencoba kembali", - "next": "Berikutnya", - "previous": "Sebelumnya", - "show-all": "Tampilkan semua", - "delete": "Hapus", - "delete-all": "Hapus semua", - "save": "Simpan", - "save-success": "Simpan berhasil", - "logout": "Logout", - "login": "Login", - "no-data": "Tidak ada data", - "clear": "Bersihkan", - "export": "Ekspor", - "off": "Off", - "error-message": "Pesan Kesalahan", - "disconnect": "Terputus" - }, - "home": { - "continue-watching": "Lanjutkan", - "favorite": "Favorite", - "no-record": "Tidak ada Favorit or Riwayat tampilan", - "watched": "Menonton {ep}", - "favorite-all": "Semua favorit {type}" - }, - "search": { - "hint-text": "Silakan gunakan pencarian dengan bijak!~", - "all": "Semua", - "filter": "Filter" - }, - "extension": { - "import": { - "title": "Import Ekstensi", - "url-label": "URL Ekstensi", - "tips": "Kamu dapat mengimpor ekstensi melalui URL, atau mengeklik direktori ekstensi di bawah dan meletakkan file ekstensi di sana.", - "extension-dir": "Direktori Ekstensi", - "import-by-url": "Impor berdasarkan URL" - }, - "error-dialog": "Pesan Kesalahan", - "installed": "Terinstall", - "edit-code": "Edit Kode" - }, - "extension-repo": { - "error": "Terjadi kesalahan!", - "error-tips": "Silakan periksa koneksi jaringan Anda atau URL repositori", - "empty": "Repositori kosong", - "upgrade": "Update" - }, - "settings": { - "general": "General", - "general-subtitle": "TMDB, Bahasa, Tema, periksa untuk update...", - "extension": "Ekstensi", - "extension-subtitle": "Ekstensi repository", - "video-player": "Pemutar video", - "video-player-subtitle": "Server BT, Pemutar Eksternal...", - "comic-reader": "Pembaca Komik", - "comic-reader-subtitle": "Mode pembaca default...", - "tracking": "Pelacakan", - "tracking-subtitle": "AniList...", - "auto-tracking": "Pelacakan Otomatis", - "auto-tracking-subtitle": "Sinkronkan kemajuan secara otomatis setelah selesai menonton / membaca", - "about": "Tentang", - "links": "Links", - "contributors": "Kontributor", - "repo-url": "Repository URL", - "repo-url-subtitle": "Dapatkan URL repositori untuk ekstensi", - "tmdb-key": "TMDB API Key", - "tmdb-key-subtitle": "Dapatkan API key untuk metadata TMDB", - "bt-server": "BT Server", - "bt-server-subtitle": "BT Server adalah komponen penting untuk pemutaran torrent onlinet", - "bt-server-manager": "Manage", - "upgrade": "Perbarui Aplikasi", - "upgrade-subtitle": "Versi: {version}", - "upgrade-training": "Memeriksa", - "auto-check-update": "Periksa Pembaruan Secara Otomatis", - "auto-check-update-subtitle": "Periksa pembaruan pada setiap startup aplikasi", - "language": "Bahasa", - "theme": "Tema", - "theme-subtitle": "Ubah tema aplikasi", - "theme-system": "System", - "theme-light": "Light", - "theme-dark": "Dark", - "theme-black": "Black", - "nsfw": "NSFW", - "nsfw-subtitle": "Tampilkan konten NSFW", - "external-player": "Pemutar video eksternal", - "external-player-subtitle": "Saat ini, pemutar video adalah {player}", - "external-player-builtin": "Built-in", - "language-subtitle": "Pilih bahasa aplikasi", - "extension-log": "Extension Log Window", - "extension-log-subtitle": "Used for debugging extensions", - "skip-interval": "Lewati Interval", - "skip-interval-subtitle": "Melewati interval untuk pemutar video internal", - "default-reader-mode": "Default reader mode", - "network": "Jaringan", - "network-subtitle": "Proxy, User-Agent...", - "network-ua": "Webview User-Agent", - "network-ua-subtitle": "Ubah Agen-Pengguna di header request untuk Web dan ekstensi.", - "proxy-type": "Tipe Proxy", - "proxy-type-subtitle": "Jenis proxy untuk permintaan", - "proxy-type-direct": "Direct", - "proxy-type-socks4": "Socks4", - "proxy-type-socks5": "Socks5", - "proxy-type-http": "HTTP", - "proxy": "Proxy", - "proxy-subtitle": "Proxy address (e.g. username:password@host:port)", - "log": "Log", - "log-subtitle": "Simpan log, ekspor log...", - "save-log": "Simpan Log", - "save-log-subtitle": "Simpan file log secara otomatis", - "export-log": "Export Log", - "export-log-subtitle": "Ekspor log ke file", - "advanced": "Lanjutan" - }, - "external-player-launching": "Menjalankan {player}", - "detail": { - "tracking": "Pelacakan", - "favorite": "Favorit", - "favorited": "Favorit", - "continue-watching": "Lanjutkan {episode}", - "total-episodes": "Total {total}", - "overview": "Ringkasan", - "cast": "Cast", - "additional-info": "Informasi Tambahan", - "get-lastest-data-error": "Gagal mendapatkan data terbaru, coba lagi nanti", - "modify-tmdb-binding": "Ubah binding data TMDB", - "no-tmdb-data": "No TMDB data matched, please bind the data yourself", - "tmdb-key-missing": "Kunci API TMDB hilang, silakan isi pengaturannya", - "tracker": "Pelacak" - }, - "video": { - "episodes": "Episode", - "watch-now": "Tonton Sekarang", - "no-episodes": "Tidak ada episode", - "play-complete": "Pemutaran selesai", - "resume-last-playback": "Lanjutkan pemutaran terakhir", - "subtitle-none": "Tidak ada Subtitle", - "subtitle": "Subtitle", - "subtitle-change": "Ubah Subtitle {title}", - "subtitle-file": "Tambahkan File subtitle", - "torrent-downloading": "Torrent downloading", - "no-qualities": "Tidak ada kualitas yang tersedia", - "audio": "Audio", - "getting-streamlink": "Mendapatkan streamlink...", - "streamlink-error": "Gagal mendapatkan streamlink", - "tooltip": { - "close": "Tutup", - "subtitle": "Subtitle", - "play-list": "Play List", - "quality": "Kualitas", - "speed": "Kecepatan", - "play": "Play", - "play-or-pause": "Pause", - "previous": "Previous", - "next": "Next", - "full-screen": "Full Screen", - "volume": "Volume", - "torrent-file-list": "Torrent file list" - }, - "cast": "Cast to device", - "cast-device": "diputar di {perangkat}", - "sidebar": { - "tab": { - "episodes": "Episode", - "qualitys": "Kualitas", - "torrentFiles": "Torrent Files", - "tracks": "Tracks", - "settings": "Pengaturan" - }, - "subtitle": { - "title": "Subtitle", - "font-size": "Ukuran Font", - "font-color": "Warna Font", - "background-color": "Warna Background", - "background-opacity": "Opacity Background", - "text-align": "Text align", - "font-weight": "Font weight", - "font-weight-normal": "Normal", - "font-weight-bold": "Bold" - }, - "play-mode": { - "title": "Modus Putar", - "loop": "Loop", - "single": "Single", - "auto-next": "Auto next" - } - } - }, - "comic-settings": { - "read-mode": "Modus baca", - "standard": "Standar", - "right-to-left": "Kanan ke kiri", - "web-tonn": "Webtoon" - }, - "novel-settings": { - "font-size": "Ukuran Font" - }, - "bugreport": { - "auto-remove-subtitle": "di hapus dalam ~ hari", - "show-report-dialog": "Tampilkan Dialog Laporan", - "show-report-dialog-subtitle": "Tampilkan dialog laporan kesalahan saat aplikasi dimulai" - }, - "reader": { - "chapters": "Bab", - "read-now": "Baca Sekarang", - "no-chapters": "Tidak ada bab" - }, - "upgrade": { - "check-update": "Periksa pembaruan", - "new-version": "Versi baru {version} terdeteksi", - "download": "Buka Pembaruan", - "no-update": "Tidak ada pembaruan yang tersedia", - "not-now": "Tidak sekarang", - "error": "Gagal memeriksa pembaruan, terjadi kesalahan jaringan" - }, - "extension-install-error": "Gagal memasang ekstensi", - "extension-type": { - "video": "Vidio", - "novel": "Novel", - "comic": "Komik" - }, - "extension-info": { - "author": "Author", - "description": "Deskripsi", - "version": "Versi", - "language": "Bahasa", - "original-site": "Situs Asli", - "other-information": "Informasi Lainnya", - "license": "Lisensi", - "title": "Info Ekstensi" - }, - "cookie-clean": { - "title": "Cookie Clean", - "subtitle": "Kamu mungkin perlu login lagi setelah pembersihan", - "success": "Clean success", - "clean": "Clean" - }, - "tmdb": { - "backdrops": "Backdrops", - "status": "Status", - "original-title": "Judul Asli", - "release-date": "Tanggal Rilis", - "genres": "Genr", - "runtime": "Runtime", - "languages": "Bahasa" - }, - "bt-server": { - "not-installed": "BT-Server tidak diinstal", - "running": "BT-Server sedang berjalan", - "stopped": "BT-Server telah berhenti", - "version": "Versi {version}", - "remote-version": "Versi remote {version}", - "stop": "Stop", - "upgrade": "Upgrade", - "start": "Start" - }, - "report": { - "copied": "Disalin ke clipboard", - "github-bug-report": "Laporkan Bug di Github", - "title": "Laporkan Bug", - "copy-message": "Salin pesan kesalahan", - "show-report-checkbox": "Tampilkan dialog laporan saat aplikasi dimulai" - }, - "anilist": { - "title": "AniList Tracking", - "login-hint-1": "Sepertinya kamu belum Login ke AniList", - "login-hint-2": "Silakan login ke AniList terlebih dahulu", - "status": "Status", - "score": "Skor", - "watching": "Menonton", - "reading": "Membaca", - "completed": "Lengkap", - "paused": "Paused", - "dropped": "Dropped", - "planning": "Planning", - "hold-on": "Hold on", - "re-watching": "Re-watching", - "re-reading": "Re-reading", - "start-date": "Tanggal Mulai", - "end-date": "Tanggal Selesai", - "unbind": "Unbind", - "episodes": "Episode", - "manga-chapter-read": "Bacaan Bab Manga: {bab}", - "anime-episode-watch": "Episode Anime yang Ditonton: {episodes}" - } -} diff --git a/assets/i18n/pl.json b/assets/i18n/pl.json deleted file mode 100644 index b2bc89fb..00000000 --- a/assets/i18n/pl.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "common": { - "home": "Strona główna", - "search": "Szukaj", - "extension": "Rozszerzenia", - "extension-repo": "Wszystkie rozszerzenia", - "settings": "Ustawienia", - "no-extension": "Brak zainstalowanych rozszerzeń", - "no-result": "Nie znaleziono", - "no-more-data": "To już wszystko :3", - "cancel": "Anuluj", - "confirm": "Akceptuj", - "close": "Zamknij", - "copied": "Skopiowano do schowka", - "uninstall": "Odinstaluj", - "install": "Zainstaluj", - "repo": "Repozytorium", - "unset": "Nie ustawiono", - "extension-missing": "Brakujące rozszerzenie: {package}", - "error": "Błąd", - "retry": "Ponów", - "next": "Następne", - "previous": "Poprzednie", - "show-all": "Pokaż wszystkie", - "delete": "Usuń", - "delete-all": "Usuń wszystko", - "save": "Zapisz", - "save-success": "Zapisano pomyślnie", - "logout": "Wyloguj", - "login": "Zaloguj", - "no-data": "Brak danych", - "clear": "Wyczyść", - "export": "Wyeksportuj" - }, - "home": { - "continue-watching": "Kontynuuj", - "favorite": "Ulubione", - "no-record": "Brak ulubionych/ostatnio przeglądanych serii", - "watched": "Ostatnio obejrzano: {ep}", - "favorite-all": "All {type} favorites" - }, - "search": { - "hint-text": "Prosimy o rozważne używanie szukajki!~", - "all": "Wszystkie", - "filter": "Filtry" - }, - "extension": { - "import": { - "title": "Zaimportuj rozszerzenie", - "url-label": "URL rozszerzenia", - "tips": "Możesz zaimportować rozszerzenie poprzez link, bądź dodając plik do katalogu wskazanego poniżej (adres kopiuje się do schowka).", - "extension-dir": "Katalog do rozszerzeń", - "import-by-url": "Import za pomocą URL" - }, - "error-dialog": "Wystąpił błąd!", - "installed": "Zainstalowano pomyślnie", - "edit-code": "Edytuj kod rozszerzenia" - }, - "extension-repo": { - "error": "Wystąpił błąd!", - "error-tips": "Sprawdź stan połączenia z internetem i/lub adres repozytorium", - "empty": "Repozytorium jest puste", - "upgrade": "Aktualizuj" - }, - "settings": { - "general": "Ogólne", - "general-subtitle": "TMDB, Język, Motyw, Sprawdź dostępność aktualizacji...", - "extension": "Rozszerzenia", - "extension-subtitle": "Ustawienia repozytorium rozszerzeń", - "video-player": "Odtwarzacz wideo", - "video-player-subtitle": "Serwer BT, Odtwarzacz zewnętrzny...", - "comic-reader": "Czytnik komiksów", - "comic-reader-subtitle": "Domyślny tryb czytnika...", - "tracking": "Śledzenie postępu", - "tracking-subtitle": "AniList...", - "auto-tracking": "Automatyczne śledzenie postępu", - "auto-tracking-subtitle": "Automatycznie synchronizuj postęp po zakończeniu oglądania / czytania", - "about": "O aplikacji", - "links": "Linki", - "contributors": "O twórcach", - "repo-url": "URL repozytorium rozszerzeń", - "repo-url-subtitle": "Get the repository URL for extensions", - "tmdb-key": "Klucz API dla TMDB", - "tmdb-key-subtitle": "Pobierz klucz API dla metadanych TMDB", - "bt-server": "Serwer BT", - "bt-server-subtitle": "Serwer BT jest niezbędnym składnikiem w przypadku odtwarzania torrentów online", - "bt-server-manager": "Zarządzaj", - "upgrade": "Zaktualizuj aplikację", - "upgrade-subtitle": "Wersja: {version}", - "upgrade-training": "Sprawdź dostępność", - "auto-check-update": "Automatyczne sprawdzanie dostępności aktualizacji", - "auto-check-update-subtitle": "Sprawdzaj dostępność aktualizacji przy włączaniu aplikacji", - "language": "Język", - "theme": "Motyw", - "theme-subtitle": "Zmień schemat kolorystyczny aplikacji", - "theme-system": "Zgodnie z systemem", - "theme-light": "Jasny", - "theme-dark": "Ciemny", - "theme-black": "Czarny (AMOLED)", - "nsfw": "NSFW", - "nsfw-subtitle": "Pokazuj treści NSFW", - "external-player": "Preferowany odtwarzacz wideo", - "external-player-subtitle": "Obecny preferowany odtwarzacz: {player}", - "external-player-builtin": "Wbudowany", - "language-subtitle": "Zmień język aplikacji", - "extension-log": "Okno z logami rozszerzeń", - "extension-log-subtitle": "Zazwyczaj używane do debugowania rozszerzeń", - "skip-interval": "Ilość czasu do przewijania", - "skip-interval-subtitle": "Co ile ma ", - "default-reader-mode": "Domyślny tryb czytnika", - "network": "Sieć", - "network-subtitle": "Proxy, User-Agent...", - "network-ua": "User-Agent", - "network-ua-subtitle": "Zmień User-Agent string w nagłówku żądania dla webview i rozszerzeń.", - "proxy-type": "Typ proxy", - "proxy-type-subtitle": "Typ proxy dla żądań", - "proxy-type-direct": "Direct", - "proxy-type-socks4": "Socks4", - "proxy-type-socks5": "Socks5", - "proxy-type-http": "HTTP", - "proxy": "Proxy", - "proxy-subtitle": "Adres proxy (np. nazwaUżytkownika:hasło@adresHosta:port)", - "log": "Logi", - "log-subtitle": "Zapisz logi, wyeksportuj logi...", - "save-log": "Zapisz logi", - "save-log-subtitle": "Automatyczne zapisywanie pliku logów", - "export-log": "Wyeksportuj logi", - "export-log-subtitle": "Wyeksportuj logi do pliku", - "advanced": "Zaawansowane" - }, - "external-player-launching": "Uruchamianie {player}", - "detail": { - "favorite": "Ulubione", - "favorited": "W ulubionych", - "continue-watching": "Kontynuuj: {episode}", - "total-episodes": "Wszystkich: {total}", - "overview": "Opis", - "cast": "Obsada", - "additional-info": "Dodatkowe inforamcje", - "get-lastest-data-error": "Błąd aktualizacji danych", - "modify-tmdb-binding": "Zmień powiązanie dla TMDB", - "no-tmdb-data": "Nie znaleziono odpowiadającego tytułu na TMDB, powiąż tytuł samodzielnie", - "tmdb-key-missing": "Brak klucza API dla TMDB. Dodaj go w ustawieniach aplikacji." - }, - "video": { - "episodes": "Epizody", - "watch-now": "Zacznij oglądanie", - "no-episodes": "Brak epizodów", - "play-complete": "Koniec i bomba. A kto czytał ten trąba. W. G.", - "resume-last-playback": "Kontynuuj oglądanie", - "subtitle-none": "Brak napisów", - "subtitle": "Napisy", - "subtitle-change": "Zmień napisy dla {title}", - "subtitle-file": "Plik z napisami", - "torrent-downloading": "Pobierz z torrentów", - "tooltip": { - "close": "Zamknij", - "subtitle": "Napisy", - "play-list": "Lista odtwarzania", - "quality": "Jakość", - "speed": "Szybkość", - "play": "Odtwarzaj", - "play-or-pause": "Pauza", - "previous": "Poprzedni", - "next": "Następny", - "full-screen": "Tryb pełnoekranowy", - "volume": "Głośność", - "torrent-file-list": "Lista torrentów" - } - }, - "comic-settings": { - "read-mode": "Tryb czytania", - "standard": "Standardowy", - "right-to-left": "Manga", - "web-tonn": "Manhwa" - }, - "novel-settings": { - "font-size": "Rozmiar tekstu" - }, - "bugreport": { - "auto-remove-subtitle": "Usuń po ~ dniach", - "show-report-dialog": "Pokaż dymek z raportem", - "show-report-dialog-subtitle": "Pokaż dymek z raportem o błędach podczas uruchamiania aplikacji" - }, - "reader": { - "chapters": "Rozdziały", - "read-now": "Zacznij czytanie", - "no-chapters": "Brak rozdziałów" - }, - "upgrade": { - "check-update": "Sprawdź dostępność aktualizacji", - "new-version": "Znaleziono nową wersję: {version}", - "download": "Zaktualizuj", - "no-update": "Używasz najnowszej dostępnej wersji aplikacji", - "not-now": "Nie teraz", - "error": "Sprawdzenie dostępności aktualizacji zakończone niepowodzeniem, błąd połączenia z internetem" - }, - "extension-install-error": "Instalacja rozszerzenia zakończona niepowodzeniem", - "extension-type": { - "video": "Wideo", - "novel": "Nowelki", - "comic": "Komiksy" - }, - "extension-info": { - "author": "Autor", - "description": "Opis", - "version": "Wersja", - "language": "Język", - "original-site": "Strona internetowa", - "other-information": "Dodatkowe informacje", - "license": "Licencja", - "title": "Informacje o rozszerzeniu" - }, - "cookie-clean": { - "title": "Wyczyść pliki cookie", - "subtitle": "Po tej operacji możesz być zmuszony do ponownego zalogowania się", - "success": "Czyszczenie zakończone sukcesem", - "clean": "Czysto" - }, - "tmdb": { - "backdrops": "Tła", - "status": "Status", - "original-title": "Tytuł oryginalny", - "release-date": "Data wydania", - "genres": "Gatunki", - "runtime": "Czas trwania", - "languages": "Języki" - }, - "bt-server": { - "not-installed": "Nie zainstalowano Serwera BT", - "running": "Serwer BT działa", - "stopped": "Zatrzymano działanie Serwera BT", - "version": "Wersja {version}", - "remote-version": "Wersja zdalna {version}", - "stop": "Zatrzymaj", - "upgrade": "Zaktualizuj", - "start": "Rozpocznij" - }, - "report": { - "copied": "Skopiowano do schowka", - "github-bug-report": "Zgłoszono jako GitHub issue", - "title": "Zgłoszenie błędu", - "copy-message": "Skopiuj wiadomość o błędzie", - "show-report-checkbox": "Pokazuj informacje o błędach przy włączaniu aplikacji" - }, - "anilist": { - "title": "Śledzenie dla AniLista", - "login-hint-1": "Wygląda na to że nie zalogowałeś się do swojego konta na AniLiście", - "login-hint-2": "Zaloguj się najpierw do AniLista", - "status": "Status", - "score": "Ocena", - "watching": "W trakcie oglądania", - "reading": "W trakcie czytania", - "completed": "Skończone", - "paused": "Na wstrzymaniu", - "dropped": "Porzucone", - "planning": "W planach", - "hold-on": "Hold on", - "re-watching": "Oglądane ponownie", - "re-reading": "Czytane ponownie", - "start-date": "Data rozpoczęcia", - "end-date": "Data zakończenia", - "unbind": "Rozłącz", - "episodes": "Epizody", - "manga-chapter-read": "Przeczytano rozdziałów: {chapters}", - "anime-episode-watch": "Obejrzano epizodów: {episodes}" - } -} diff --git a/lib/data/services/extension_jscore_plugin.dart b/lib/data/services/extension_jscore_plugin.dart deleted file mode 100644 index 510ac650..00000000 --- a/lib/data/services/extension_jscore_plugin.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter_js/flutter_js.dart'; -import 'package:miru_app/utils/log.dart'; - -const dartBridgeMessageName = 'DART_BRIDGE_MESSAGE_NAME'; - -class JsBridge { - final JavascriptRuntime jsRuntime; - int _messageCounter = 0; - static final Map _pendingRequests = {}; - final Object? Function(Object? value)? toEncodable; - static final Map Function(dynamic message)> - _handlers = {}; - JsBridge({ - required this.jsRuntime, - this.toEncodable, - }) { - final bridgeScriptEvalResult = jsRuntime.evaluate(jsBridgeJs); - if (bridgeScriptEvalResult.isError) { - logger.info('Error eval bridge script'); - } - final windowEvalResult = - jsRuntime.evaluate('var window = global = globalThis;'); - if (windowEvalResult.isError) { - logger.info('Error eval window script'); - } - jsRuntime.onMessage(dartBridgeMessageName, (message) { - _onMessage(message); - }); - } - - _onMessage(dynamic message) async { - if (message['isRequest']) { - final handler = _handlers[message['name']]; - if (handler == null) { - logger.info('Error: no handlers for message $message'); - } else { - final result = await handler(message['args']); - final jsResult = jsRuntime.evaluate( - 'onMessageFromDart(false, ${message['callId']}, "${message['name']}",${jsonEncode(result, toEncodable: toEncodable)})'); - if (jsResult.isError) { - logger.info('Error sending message to JS: $jsResult'); - } - } - } else { - final completer = _pendingRequests.remove(message['callId']); - if (completer == null) { - logger.info('Error: no completer for response for message $message'); - } else { - completer.complete(message['result']); - } - } - } - - sendMessage(String name, dynamic message) async { - if (_messageCounter > 999999999) { - _messageCounter = 0; - } - _messageCounter += 1; - final completer = Completer(); - _pendingRequests[_messageCounter] = completer; - final jsResult = jsRuntime.evaluate( - 'window.onMessageFromDart(true, $_messageCounter, "$name",${jsonEncode(message, toEncodable: toEncodable)})'); - if (jsResult.isError) { - logger.info('Error sending message to JS: $jsResult'); - } - - return completer.future; - } - - // final _handlers = {}; - - setHandler(String name, Future Function(dynamic message) handler) { - _handlers[name] = handler; - } -} - -const jsBridgeJs = ''' -globalThis.DartBridge = (() => { - let callId = 0; - const DART_BRIDGE_MESSAGE_NAME = '$dartBridgeMessageName'; - globalThis.onMessageFromDart = async (isRequest, callId, name, args) => { - if (isRequest) { - if (handlers[name]) { - sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({ - isRequest: false, - callId, - name, - result: await handlers[name](args), - })); - } - } - else { - const pendingResolve = pendingRequests[callId]; - delete pendingRequests[callId]; - if (pendingResolve) { - pendingResolve(args); - } - } - return null; - }; - const handlers = {}; - const pendingRequests = {}; - return { - sendMessage: async (name, args) => { - if (callId > 999999999) { - callId = 0; - } - callId += 1; - sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({ - isRequest: true, - callId, - name, - args, - }),call=((res)=>{})); - return new Promise((resolve) => { - pendingRequests[callId] = resolve; - call(resolve) - }); - }, - setHandler: (name, handler) => { - handlers[name] = handler; - }, - resolveRequest: (callId, result) => { - sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({ - isRequest: false, - callId, - result, - })); - }, - }; -})(); -global = globalThis; -'''; diff --git a/lib/views/pages/watch/video/video_player_cast.dart b/lib/views/pages/watch/video/video_player_cast.dart deleted file mode 100644 index c7627938..00000000 --- a/lib/views/pages/watch/video/video_player_cast.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:dlna_dart/dlna.dart'; -import 'package:miru_app/utils/i18n.dart'; -import 'package:miru_app/utils/log.dart'; -import 'package:miru_app/views/widgets/progress.dart'; - -class VideoPlayerCast extends StatefulWidget { - const VideoPlayerCast({ - super.key, - this.onDeviceSelected, - }); - final Function(DLNADevice device)? onDeviceSelected; - - @override - State createState() => _VideoPlayerCastState(); -} - -class _VideoPlayerCastState extends State { - late DLNAManager searcher; - Map deviceList = {}; - - @override - void initState() { - super.initState(); - _init(); - } - - _init() async { - searcher = DLNAManager(); - logger.info('DLNA searching devices...'); - final m = await searcher.start(); - m.devices.stream.listen((deviceList) { - logger.info('DLNA devices: $deviceList'); - setState(() { - this.deviceList = deviceList; - }); - }); - } - - @override - void dispose() { - logger.info('DLNA stop searching devices...'); - searcher.stop(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'video.cast'.i18n, - style: const TextStyle( - fontSize: 18, - ), - ), - ), - const SizedBox(height: 10), - for (final device in deviceList.entries) - ListTile( - title: Text(device.value.info.friendlyName), - subtitle: Text(device.key), - onTap: () async { - widget.onDeviceSelected?.call(device.value); - }, - ), - const Center( - child: ProgressRing(), - ) - ], - ); - } -} diff --git a/lib/views/pages/watch/video/video_player_desktop_controls.dart b/lib/views/pages/watch/video/video_player_desktop_controls.dart deleted file mode 100644 index c07660c7..00000000 --- a/lib/views/pages/watch/video/video_player_desktop_controls.dart +++ /dev/null @@ -1,1183 +0,0 @@ -import 'dart:async'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; -import 'package:miru_app/controllers/watch/video_controller.dart'; -import 'package:miru_app/router/router.dart'; -import 'package:miru_app/utils/i18n.dart'; -import 'package:miru_app/views/widgets/cache_network_image.dart'; -import 'package:miru_app/views/widgets/watch/playlist.dart'; -import 'package:window_manager/window_manager.dart'; - -class VideoPlayerDesktopControls extends StatefulWidget { - const VideoPlayerDesktopControls({ - super.key, - required this.controller, - }); - final VideoPlayerController controller; - - @override - State createState() => - _VideoPlayerDesktopControlsState(); -} - -class _VideoPlayerDesktopControlsState - extends State { - late final _c = widget.controller; - final FocusNode _focusNode = FocusNode(); - Timer? _timer; - bool _showControls = true; - final _subtitleViewKey = GlobalKey(); - - _updateTimer() { - _timer?.cancel(); - _timer = null; - setState(() { - _showControls = true; - }); - _timer = Timer.periodic( - const Duration(seconds: 3), - (_) { - if (mounted) { - setState(() { - _showControls = false; - }); - } - }, - ); - } - - @override - void initState() { - super.initState(); - _updateTimer(); - } - - @override - void dispose() { - _timer?.cancel(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onHover: (_) { - _updateTimer(); - }, - child: FluentTheme( - data: FluentThemeData( - brightness: Brightness.dark, - ), - child: KeyboardListener( - focusNode: _focusNode, - autofocus: true, - onKeyEvent: (value) { - if (value is KeyDownEvent) { - _c.keyboardShortcuts[value.logicalKey]?.call(); - } - }, - child: Stack( - children: [ - // subtitle - Positioned.fill( - child: Obx( - () { - final textStyle = TextStyle( - height: 1.4, - fontSize: _c.subtitleFontSize.value, - letterSpacing: 0.0, - wordSpacing: 0.0, - color: _c.subtitleFontColor.value, - fontWeight: _c.subtitleFontWeight.value, - backgroundColor: - _c.subtitleBackgroundColor.value.withOpacity( - _c.subtitleBackgroundOpacity.value, - ), - ); - _subtitleViewKey.currentState?.textAlign = - _c.subtitleTextAlign.value; - _subtitleViewKey.currentState?.style = textStyle; - _subtitleViewKey.currentState?.padding = - EdgeInsets.fromLTRB( - 16.0, - 0.0, - 16.0, - _showControls ? 100.0 : 16.0, - ); - return SubtitleView( - controller: _c.videoController, - configuration: SubtitleViewConfiguration( - style: textStyle, - textAlign: _c.subtitleTextAlign.value, - ), - key: _subtitleViewKey, - ); - }, - ), - ), - Positioned.fill( - child: SizedBox.expand( - child: Center( - child: Obx(() { - if (_c.error.value.isNotEmpty) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'video.streamlink-error'.i18n, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Button( - child: Text('common.error-message'.i18n), - onPressed: () { - showDialog( - context: context, - builder: (context) => ContentDialog( - constraints: const BoxConstraints( - maxWidth: 500, - ), - title: - Text('common.error-message'.i18n), - content: SelectableText(_c.error.value), - actions: [ - Button( - child: Text('common.close'.i18n), - onPressed: () { - router.pop(); - }, - ), - ], - ), - ); - }, - ), - const SizedBox(width: 10), - Button( - child: Text('common.retry'.i18n), - onPressed: () { - _c.error.value = ''; - _c.play(); - }, - ), - ], - ) - ], - ); - } - if (!_c.isGettingWatchData.value) { - return StreamBuilder( - stream: _c.player.stream.buffering, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data! || - _c.player.state.buffering) { - return const ProgressRing(); - } - return const SizedBox.shrink(); - }, - ); - } - return Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_c.runtime.extension.icon != null) - Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.only(right: 10), - child: CacheNetWorkImagePic( - _c.runtime.extension.icon!, - width: 30, - height: 30, - ), - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _c.runtime.extension.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'video.getting-streamlink'.i18n, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ), - ], - ) - ], - ), - ); - }), - ), - ), - ), - Positioned.fill( - child: Column( - children: [ - // header - Opacity( - opacity: _showControls ? 1 : 0, - child: _Header( - title: _c.title, - episode: _c.playList[_c.index.value].name, - onClose: () { - if (_c.isFullScreen.value) { - WindowManager.instance.setFullScreen(false); - } - router.pop(); - }, - ), - ), - // center - const Spacer(), - // footer - Opacity( - opacity: _showControls ? 1 : 0, - child: _Footer(controller: _c), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -class _Header extends StatefulWidget { - const _Header({ - required this.title, - required this.episode, - required this.onClose, - }); - final String title; - final String episode; - final VoidCallback onClose; - - @override - State<_Header> createState() => _HeaderState(); -} - -class _HeaderState extends State<_Header> { - bool _isAlwaysOnTop = false; - - @override - initState() { - super.initState(); - WindowManager.instance.isAlwaysOnTop().then((value) { - _isAlwaysOnTop = value; - }); - } - - @override - void dispose() { - if (_isAlwaysOnTop) { - WindowManager.instance.setAlwaysOnTop(false); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.black.withOpacity(0.5), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Expanded( - child: DragToMoveArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - widget.episode, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - // 置顶 - IconButton( - icon: Icon( - _isAlwaysOnTop ? FluentIcons.pinned : FluentIcons.pin, - ), - onPressed: () async { - WindowManager.instance.setAlwaysOnTop( - !_isAlwaysOnTop, - ); - setState(() { - _isAlwaysOnTop = !_isAlwaysOnTop; - }); - }, - ), - const SizedBox(width: 10), - IconButton( - icon: const Icon( - FluentIcons.chrome_minimize, - ), - onPressed: () { - WindowManager.instance.minimize(); - }, - ), - const SizedBox(width: 10), - IconButton( - onPressed: widget.onClose, - icon: const Icon( - FluentIcons.chevron_down, - ), - ), - ], - ), - ), - ); - } -} - -class _Footer extends StatelessWidget { - const _Footer({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.black.withOpacity(0.5), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Column( - children: [ - Row( - children: [ - // 当前进度 - StreamBuilder( - stream: controller.player.stream.position, - builder: (context, snapshot) { - if (snapshot.hasData) { - final position = snapshot.data as Duration; - return Text( - '${position.inMinutes}:${(position.inSeconds % 60).toString().padLeft(2, '0')}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ); - } - return const SizedBox.shrink(); - }, - ), - const SizedBox(width: 20), - Expanded( - child: _SeekBar(controller: controller), - ), - const SizedBox(width: 20), - // 总时长 - StreamBuilder( - stream: controller.player.stream.duration, - builder: (context, snapshot) { - if (snapshot.hasData) { - final duration = snapshot.data as Duration; - return Text( - '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ); - } - return const SizedBox.shrink(); - }, - ), - ], - ), - const SizedBox(height: 10), - LayoutBuilder(builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (constraints.maxWidth > 500) - Expanded( - flex: 1, - child: // 音量 - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _Volume( - value: controller.player.state.volume, - onVolumeChanged: (value) { - controller.player.setVolume(value); - }, - ), - // 画质 - Obx(() { - if (controller.currentQuality.value.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(left: 10), - child: _Quality(controller: controller), - ); - }), - ], - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 上一集 - Obx( - () => IconButton( - onPressed: controller.index.value > 0 - ? () { - controller.index.value--; - } - : null, - icon: const Icon( - FluentIcons.previous, - ), - ), - ), - const SizedBox(width: 20), - StreamBuilder( - stream: controller.player.stream.playing, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data! || - controller.player.state.playing) { - return IconButton( - onPressed: controller.player.pause, - icon: const Icon( - FluentIcons.pause, - size: 30, - ), - ); - } - return IconButton( - onPressed: controller.player.play, - icon: const Icon( - FluentIcons.play, - size: 30, - ), - ); - }, - ), - const SizedBox(width: 20), - // 下一集 - Obx( - () => IconButton( - onPressed: controller.playList.length - 1 > - controller.index.value - ? () { - controller.index.value++; - } - : null, - icon: const Icon( - FluentIcons.next, - ), - ), - ), - ], - ), - ), - if (constraints.maxWidth > 500) - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - // playback speed - if (constraints.maxWidth > 700) - Padding( - padding: const EdgeInsets.only(right: 10), - child: _Speed(controller: controller), - ), - // torrent files - if (constraints.maxWidth > 700) - Obx(() { - if (controller.torrentMediaFileList.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(right: 10), - child: _TorrentFiles( - controller: controller, - ), - ); - }), - // track - _Track(controller: controller), - const SizedBox(width: 10), - - // 剧集 - _Episode(controller: controller), - const SizedBox(width: 10), - // 全屏 - Obx( - () => IconButton( - onPressed: () { - controller.toggleFullscreen(); - }, - icon: Icon( - controller.isFullScreen.value - ? FluentIcons.back_to_window - : FluentIcons.full_screen, - ), - ), - ), - const SizedBox(width: 10), - // 设置 - IconButton( - onPressed: () { - final showPlayList = controller.showSidebar.value; - controller.showSidebar.value = !showPlayList; - }, - icon: const Icon( - FluentIcons.settings, - ), - ), - ], - ), - ), - ], - ); - }) - ], - ), - ), - ); - } -} - -class _Volume extends StatefulWidget { - const _Volume({ - required this.value, - required this.onVolumeChanged, - }); - final double value; - final Function(double value) onVolumeChanged; - - @override - State<_Volume> createState() => _VolumeState(); -} - -class _VolumeState extends State<_Volume> { - final _controller = FlyoutController(); - final _volume = 0.0.obs; - @override - void initState() { - super.initState(); - _volume.value = widget.value; - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlyoutTarget( - controller: _controller, - child: IconButton( - icon: Obx( - () => Icon( - _volume.value == 0 - ? FluentIcons.volume0 - : _volume.value < 50 - ? FluentIcons.volume1 - : _volume.value < 100 - ? FluentIcons.volume2 - : FluentIcons.volume3, - ), - ), - onPressed: () { - _controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - useAcrylic: true, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () => Icon( - _volume.value == 0 - ? FluentIcons.volume0 - : _volume.value < 50 - ? FluentIcons.volume1 - : _volume.value < 100 - ? FluentIcons.volume2 - : FluentIcons.volume3, - ), - ), - const SizedBox(width: 10), - Obx( - () => SizedBox( - height: 30, - child: Slider( - value: _volume.value, - max: 100, - onChanged: (value) { - _volume.value = value; - widget.onVolumeChanged(value); - }, - ), - ), - ), - const SizedBox(width: 10), - Obx( - () => Text( - _volume.value.toStringAsFixed(1), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _Episode extends StatefulWidget { - const _Episode({ - required this.controller, - }); - - final VideoPlayerController controller; - - @override - State<_Episode> createState() => _EpisodeState(); -} - -class _EpisodeState extends State<_Episode> { - final controller = FlyoutController(); - - @override - dispose() { - super.dispose(); - controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutTarget( - controller: controller, - child: IconButton( - icon: const Icon(FluentIcons.playlist_music), - onPressed: () { - controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - padding: const EdgeInsets.all(0), - useAcrylic: true, - child: Container( - width: 300, - constraints: const BoxConstraints( - maxHeight: 500, - ), - child: PlayList( - title: widget.controller.title, - list: widget.controller.playList - .map((e) => e.name) - .toList(), - selectIndex: widget.controller.index.value, - onChange: (value) { - widget.controller.index.value = value; - router.pop(); - }, - ), - ), - ), - ); - }, - ); - }, - ), - ), - ); - } -} - -class _Quality extends StatefulWidget { - const _Quality({ - required this.controller, - }); - - final VideoPlayerController controller; - - @override - State<_Quality> createState() => _QualityState(); -} - -class _QualityState extends State<_Quality> { - final controller = FlyoutController(); - - @override - dispose() { - super.dispose(); - controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlyoutTarget( - controller: controller, - child: Button( - child: Text(widget.controller.currentQuality.value), - onPressed: () { - if (widget.controller.qualityMap.isEmpty) { - widget.controller.sendMessage( - Message(Text("video.no-qualities".i18n)), - ); - return; - } - controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - useAcrylic: true, - child: Container( - width: 200, - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: ListView( - children: [ - for (final quality - in widget.controller.qualityMap.entries) - ListTile( - title: Text(quality.key), - onPressed: () { - widget.controller.switchQuality( - quality.value, - ); - router.pop(); - }, - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _Track extends StatefulWidget { - const _Track({ - required this.controller, - }); - - final VideoPlayerController controller; - - @override - State<_Track> createState() => _TrackState(); -} - -class _TrackState extends State<_Track> { - final controller = FlyoutController(); - - @override - dispose() { - super.dispose(); - controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlyoutTarget( - controller: controller, - child: IconButton( - icon: const Icon(FluentIcons.locale_language), - onPressed: () { - controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - useAcrylic: true, - padding: const EdgeInsets.all(0), - child: Container( - width: 220, - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - "video.subtitle".i18n, - style: TextStyle( - fontSize: 13, - color: Colors.white.withAlpha(200), - ), - ), - ), - const SizedBox(height: 5), - ListTile.selectable( - selected: SubtitleTrack.no() == - widget.controller.player.state.track.subtitle, - title: Text('common.off'.i18n), - onPressed: () { - widget.controller.setSubtitleTrack( - SubtitleTrack.no(), - ); - router.pop(); - }, - ), - ListTile.selectable( - title: Text('video.subtitle-file'.i18n), - onPressed: () { - widget.controller.addSubtitleFile(); - }, - ), - // 来自扩展的字幕 - for (final subtitle in widget.controller.subtitles) - ListTile.selectable( - selected: subtitle == - widget.controller.player.state.track.subtitle, - title: Text(subtitle.title ?? ''), - subtitle: Text(subtitle.language ?? ''), - onPressed: () { - widget.controller.setSubtitleTrack( - subtitle, - ); - router.pop(); - }, - ), - // 来自视频的字幕 - for (final subtitle - in widget.controller.player.state.tracks.subtitle) - if (subtitle != SubtitleTrack.no() && - (subtitle.language != null || - subtitle.title != null)) - ListTile.selectable( - selected: subtitle == - widget.controller.player.state.track.subtitle, - title: Text(subtitle.title ?? ''), - subtitle: Text(subtitle.language ?? ''), - onPressed: () { - widget.controller.setSubtitleTrack( - subtitle, - ); - router.pop(); - }, - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - "video.audio".i18n, - style: TextStyle( - fontSize: 13, - color: Colors.white.withAlpha(200), - ), - ), - ), - const SizedBox(height: 5), - // 来自视频的音轨 - for (final audio - in widget.controller.player.state.tracks.audio) - if (audio.language != null || audio.title != null) - ListTile.selectable( - selected: audio == - widget.controller.player.state.track.audio, - title: Text(audio.title ?? ''), - subtitle: Text(audio.language ?? ''), - onPressed: () { - widget.controller.player.setAudioTrack( - audio, - ); - router.pop(); - }, - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _TorrentFiles extends StatefulWidget { - const _TorrentFiles({ - required this.controller, - }); - - final VideoPlayerController controller; - - @override - State<_TorrentFiles> createState() => _TorrentFilesState(); -} - -class _TorrentFilesState extends State<_TorrentFiles> { - final controller = FlyoutController(); - - @override - dispose() { - super.dispose(); - controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlyoutTarget( - controller: controller, - child: IconButton( - icon: const Icon(FluentIcons.folder_open), - onPressed: () { - controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - useAcrylic: true, - padding: const EdgeInsets.all(0), - child: Container( - width: 300, - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - for (final file - in widget.controller.torrentMediaFileList) - ListTile.selectable( - title: Text( - file, - style: const TextStyle(fontSize: 13), - ), - selected: - widget.controller.currentTorrentFile.value == - file, - onPressed: () { - widget.controller.playTorrentFile(file); - router.pop(); - }, - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _Speed extends StatefulWidget { - const _Speed({ - required this.controller, - }); - - final VideoPlayerController controller; - - @override - State<_Speed> createState() => _SpeedState(); -} - -class _SpeedState extends State<_Speed> { - final controller = FlyoutController(); - - @override - dispose() { - super.dispose(); - controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlyoutTarget( - controller: controller, - child: Button( - child: Obx(() => Text('x${widget.controller.currentSpeed.value}')), - onPressed: () { - controller.showFlyout( - barrierDismissible: false, - dismissOnPointerMoveAway: true, - builder: (context) { - return FluentTheme( - data: FluentThemeData.dark(), - child: FlyoutContent( - useAcrylic: true, - padding: const EdgeInsets.all(0), - child: Container( - width: 200, - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - for (final speed in widget.controller.speedList) - ListTile.selectable( - title: Text( - speed.toStringAsFixed(2), - style: const TextStyle(fontSize: 13), - ), - selected: - widget.controller.currentSpeed.value == speed, - onPressed: () { - widget.controller.player.setRate(speed); - widget.controller.currentSpeed.value = speed; - router.pop(); - }, - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _SeekBar extends StatefulWidget { - const _SeekBar({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - State<_SeekBar> createState() => _SeekBarState(); -} - -class _SeekBarState extends State<_SeekBar> { - Duration position = const Duration(); - Duration duration = const Duration(); - bool _isDrag = false; - StreamSubscription? positionSubscription; - StreamSubscription? durationSubscription; - - @override - void initState() { - super.initState(); - positionSubscription = - widget.controller.player.stream.position.listen((event) { - if (!_isDrag) { - position = event; - } - }); - durationSubscription = - widget.controller.player.stream.duration.listen((event) { - duration = event; - }); - } - - @override - void dispose() { - positionSubscription?.cancel(); - durationSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Slider( - value: (position.inSeconds).toDouble(), - max: duration.inSeconds < position.inSeconds - ? position.inSeconds.toDouble() - : duration.inSeconds.toDouble(), - label: - '${position.inMinutes}:${(position.inSeconds % 60).toString().padLeft(2, '0')}', - onChanged: (value) { - _isDrag = true; - setState(() { - position = Duration(seconds: value.toInt()); - }); - }, - onChangeEnd: (value) { - _isDrag = false; - widget.controller.player.seek( - Duration( - seconds: value.toInt(), - ), - ); - }, - ); - } -} diff --git a/lib/views/pages/watch/video/video_player_mobile_controls.dart b/lib/views/pages/watch/video/video_player_mobile_controls.dart deleted file mode 100644 index 9441e68f..00000000 --- a/lib/views/pages/watch/video/video_player_mobile_controls.dart +++ /dev/null @@ -1,843 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:get/get.dart'; -import 'package:media_kit_video/media_kit_video.dart'; -import 'package:miru_app/controllers/watch/video_controller.dart'; -import 'package:miru_app/utils/i18n.dart'; -import 'package:miru_app/utils/layout.dart'; -import 'package:miru_app/utils/router.dart'; -import 'package:miru_app/views/pages/watch/video/video_player_cast.dart'; -import 'package:miru_app/views/pages/watch/video/video_player_sidebar.dart'; -import 'package:miru_app/views/widgets/cache_network_image.dart'; -import 'package:miru_app/views/widgets/progress.dart'; -import 'package:screen_brightness/screen_brightness.dart'; -import 'package:volume_controller/volume_controller.dart'; - -class VideoPlayerMobileControls extends StatefulWidget { - const VideoPlayerMobileControls({super.key, required this.controller}); - final VideoPlayerController controller; - - @override - State createState() => - _VideoPlayerMobileControlsState(); -} - -class _VideoPlayerMobileControlsState extends State { - late final VideoPlayerController _c = widget.controller; - final _subtitleViewKey = GlobalKey(); - bool _showControls = true; - double _currentBrightness = 0; - double _currentVolume = 0; - // 是否是调整亮度 - bool _isBrightness = false; - // 是否正在调节 - bool _isAdjusting = false; - // 滑动时的进度 - Duration _position = Duration.zero; - // 是否左右滑动调整进度 - bool _isSeeking = false; - // 是否长按加速 - bool _isLongPress = false; - // 定时器 - Timer? _timer; - - _updateTimer() { - _timer?.cancel(); - _timer = null; - setState(() { - _showControls = true; - }); - _timer = Timer.periodic( - const Duration(seconds: 3), - (_) { - if (mounted) { - setState(() { - _showControls = false; - }); - } - }, - ); - } - - _init() async { - _updateTimer(); - VolumeController().showSystemUI = false; - _currentBrightness = await ScreenBrightness().current; - _currentVolume = await VolumeController().getVolume(); - } - - @override - void initState() { - _init(); - super.initState(); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return DefaultTextStyle( - style: const TextStyle( - color: Colors.white, - ), - child: Theme( - data: ThemeData.dark(useMaterial3: true), - child: Stack( - children: [ - // 字幕 - Positioned.fill( - child: Obx( - () { - final textStyle = TextStyle( - height: 1.4, - fontSize: _c.subtitleFontSize.value, - letterSpacing: 0.0, - wordSpacing: 0.0, - color: _c.subtitleFontColor.value, - fontWeight: _c.subtitleFontWeight.value, - backgroundColor: - _c.subtitleBackgroundColor.value.withOpacity( - _c.subtitleBackgroundOpacity.value, - ), - ); - _subtitleViewKey.currentState?.textAlign = - _c.subtitleTextAlign.value; - _subtitleViewKey.currentState?.style = textStyle; - _subtitleViewKey.currentState?.padding = EdgeInsets.fromLTRB( - 16.0, - 0.0, - 16.0, - _showControls ? 100.0 : 16.0, - ); - return SubtitleView( - controller: _c.videoController, - configuration: SubtitleViewConfiguration( - style: textStyle, - textAlign: _c.subtitleTextAlign.value, - ), - key: _subtitleViewKey, - ); - }, - ), - ), - // 顶部提示 - Positioned( - top: 30, - left: 0, - right: 0, - child: Center( - child: Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5)), - color: Colors.black45, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_isSeeking) - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${_position.inMinutes}:${(_position.inSeconds % 60).toString().padLeft(2, '0')}', - ), - const Text('/'), - Text( - '${_c.duration.value.inMinutes}:${(_c.duration.value.inSeconds % 60).toString().padLeft(2, '0')}', - ), - ], - ), - ), - if (_isLongPress) - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Playing at 3x speed'), - ), - if (_isAdjusting) - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_isBrightness) ...[ - const Icon(Icons.brightness_5), - const SizedBox(width: 5), - Text( - (_currentBrightness * 100).toStringAsFixed(0), - ) - ], - if (!_isBrightness) ...[ - const Icon(Icons.volume_up), - const SizedBox(width: 5), - Text( - (_currentVolume * 100).toStringAsFixed(0), - ) - ], - ], - ), - ), - ], - ), - ), - ), - ), - // 手势层 - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (_showControls) { - _showControls = false; - setState(() {}); - return; - } - _updateTimer(); - }, - onDoubleTapDown: (details) { - // 如果左边点击快退,中间暂停,右边快进 - final dx = details.localPosition.dx; - final width = LayoutUtils.width / 3; - if (dx < width) { - _c.seek( - _c.position.value - const Duration(seconds: 10), - ); - } else if (dx > width * 2) { - _c.seek( - _c.position.value + const Duration(seconds: 10), - ); - } else { - _c.playOrPause(); - } - }, - onVerticalDragStart: (details) { - _isBrightness = - details.localPosition.dx < LayoutUtils.width / 2; - }, - // 左右两边上下滑动 - onVerticalDragUpdate: (details) { - final add = details.delta.dy / 500; - // 如果是左边调节亮度 - if (_isBrightness) { - _currentBrightness = (_currentBrightness - add).clamp(0, 1); - ScreenBrightness().setScreenBrightness(_currentBrightness); - } - // 如果是右边调节音量 - else { - _currentVolume = (_currentVolume - add).clamp(0, 1); - VolumeController().setVolume(_currentVolume); - } - _isAdjusting = true; - setState(() {}); - }, - onHorizontalDragStart: (details) { - _position = _c.position.value; - }, - onVerticalDragEnd: (details) { - _isAdjusting = false; - setState(() {}); - }, - // 左右滑动 - onHorizontalDragUpdate: (details) { - double scale = 200000 / LayoutUtils.width; - Duration pos = _position + - Duration( - milliseconds: (details.delta.dx * scale).round(), - ); - _position = Duration( - milliseconds: pos.inMilliseconds.clamp( - 0, - _c.duration.value.inMilliseconds, - ), - ); - _isSeeking = true; - setState(() {}); - }, - onHorizontalDragEnd: (details) { - _c.seek(_position); - _isSeeking = false; - setState(() {}); - }, - onLongPressStart: (details) { - _isLongPress = true; - _c.player.setRate(3.0); - setState(() {}); - }, - onLongPressEnd: (details) { - _c.player.setRate(_c.currentSpeed.value); - _isLongPress = false; - setState(() {}); - }, - child: const SizedBox.expand(), - ), - ), - // 中间显示 - Positioned.fill( - child: Center( - child: Obx(() { - if (_c.error.value.isNotEmpty) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "video.streamlink-error".i18n, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - FilledButton( - child: Text('common.error-message'.i18n), - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('common.error-message'.i18n), - content: SelectableText(_c.error.value), - actions: [ - FilledButton( - child: Text('common.close'.i18n), - onPressed: () { - Get.back(); - }, - ), - ], - ), - ); - }, - ), - const SizedBox(width: 10), - FilledButton( - child: Text('common.retry'.i18n), - onPressed: () { - _c.error.value = ''; - _c.play(); - }, - ), - ], - ) - ], - ); - } - if (!_c.isGettingWatchData.value) { - return StreamBuilder( - stream: _c.player.stream.buffering, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data! || - _c.player.state.buffering) { - return const ProgressRing(); - } - if (_c.dlnaDevice.value != null) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - FlutterI18n.translate( - context, - 'video.cast-device', - translationParams: { - 'device': - _c.dlnaDevice.value!.info.friendlyName, - }, - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - FilledButton( - onPressed: () { - _c.disconnectDLNADevice(); - }, - child: Text( - 'common.disconnect'.i18n, - ), - ), - ], - ); - } - return const SizedBox.shrink(); - }, - ); - } - - return Card( - color: Theme.of(context).colorScheme.surfaceVariant, - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_c.runtime.extension.icon != null) - Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.only(right: 10), - child: CacheNetWorkImagePic( - _c.runtime.extension.icon!, - width: 30, - height: 30, - ), - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _c.runtime.extension.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'video.getting-streamlink'.i18n, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ), - ], - ) - ], - ), - ), - ); - }), - ), - ), - // 头部控制栏 - if (_showControls) - Positioned( - top: 0, - left: 0, - right: 0, - child: _Header( - controller: _c, - ), - ), - // 底部控制栏 - if (_showControls) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: _Footer(controller: _c), - ), - Positioned.fill( - child: Obx( - () { - if (!_c.showSidebar.value) { - return const SizedBox.shrink(); - } - return GestureDetector( - child: Container( - color: Colors.black54, - ), - onTap: () { - _c.showSidebar.value = false; - }, - ); - }, - ), - ) - ], - ), - ), - ); - } -} - -class _Header extends StatelessWidget { - const _Header({required this.controller}); - final VideoPlayerController controller; - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black54, - Colors.transparent, - ], - ), - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - RouterUtils.pop(); - }, - ), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final data = controller.playList[controller.index.value]; - final episode = data.name; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - controller.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - episode, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - }), - ), - // DLNA - IconButton( - icon: const Icon(Icons.cast), - onPressed: () { - showModalBottomSheet( - context: context, - useSafeArea: true, - showDragHandle: true, - isScrollControlled: true, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - builder: (context, scrollController) { - return SingleChildScrollView( - controller: scrollController, - child: VideoPlayerCast( - onDeviceSelected: (device) { - controller.connectDLNADevice(device); - Get.back(); - }, - ), - ); - }, - ); - }, - ); - }, - ), - // 设置按钮 - IconButton( - icon: const Icon(Icons.settings), - onPressed: () { - controller.toggleSideBar(SidebarTab.settings); - }, - ), - ], - ), - ); - } -} - -class _Footer extends StatelessWidget { - const _Footer({required this.controller}); - final VideoPlayerController controller; - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black54, - Colors.transparent, - ], - ), - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _SeekBar(controller: controller), - const SizedBox(height: 10), - Row( - children: [ - Obx( - () => IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: controller.index.value > 0 - ? () { - controller.index.value--; - } - : null, - ), - ), - Obx(() { - if (controller.isPlaying.value) { - return IconButton( - onPressed: controller.playOrPause, - icon: const Icon( - Icons.pause, - size: 30, - ), - ); - } - return IconButton( - onPressed: controller.playOrPause, - icon: const Icon( - Icons.play_arrow, - size: 30, - ), - ); - }), - Obx( - () => IconButton( - icon: const Icon(Icons.skip_next), - onPressed: - controller.playList.length - 1 > controller.index.value - ? () { - controller.index.value++; - } - : null, - ), - ), - const SizedBox(width: 10), - // 播放进度 - Obx(() { - final position = controller.position.value; - return Text( - '${position.inMinutes}:${(position.inSeconds % 60).toString().padLeft(2, '0')}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ); - }), - const Text('/'), - Obx(() { - final duration = controller.duration.value; - return Text( - '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ); - }), - const Spacer(), - Obx(() { - if (controller.currentQuality.value.isEmpty) { - return const SizedBox.shrink(); - } - return FilledButton.tonal( - onPressed: () { - if (controller.qualityMap.isEmpty) { - controller.sendMessage( - Message( - Text( - 'video.no-qualities'.i18n, - ), - ), - ); - return; - } - controller.toggleSideBar(SidebarTab.qualitys); - }, - style: ButtonStyle( - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - ), - ), - child: Text( - controller.currentQuality.value, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ), - ); - }), - const SizedBox(width: 10), - // 倍速 - Obx( - () => PopupMenuButton( - initialValue: controller.currentSpeed.value, - onSelected: (value) { - controller.currentSpeed.value = value; - }, - itemBuilder: (context) { - return [ - for (final speed in controller.speedList) - PopupMenuItem( - value: speed, - child: Text('${speed}x'), - ), - ]; - }, - child: Text( - '${controller.currentSpeed.value}x', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w300, - ), - ), - ), - ), - // torrent files - const SizedBox(width: 10), - Obx(() { - if (controller.torrentMediaFileList.isEmpty) { - return const SizedBox.shrink(); - } - return IconButton( - onPressed: () { - controller.toggleSideBar(SidebarTab.torrentFiles); - }, - icon: const Icon(Icons.video_file), - ); - }), - IconButton( - onPressed: () { - controller.toggleSideBar(SidebarTab.tracks); - }, - icon: const Icon( - Icons.subtitles, - ), - ), - // 播放列表 - IconButton( - icon: const Icon(Icons.playlist_play), - onPressed: () { - controller.toggleSideBar(SidebarTab.episodes); - }, - ), - ], - ), - ], - ), - ); - } -} - -class _SeekBar extends StatefulWidget { - const _SeekBar({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - State<_SeekBar> createState() => _SeekBarState(); -} - -class _SeekBarState extends State<_SeekBar> { - bool _isSliderDraging = false; - Duration _position = Duration.zero; - Duration _buffer = Duration.zero; - - StreamSubscription? _bufferSubscription; - - @override - void initState() { - super.initState(); - _buffer = widget.controller.player.state.buffer; - - _bufferSubscription = - widget.controller.player.stream.buffer.listen((event) { - setState(() { - _buffer = event; - }); - }); - } - - @override - dispose() { - _bufferSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 13, - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 2, - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: 6, - ), - overlayShape: RoundSliderOverlayShape( - overlayRadius: 12, - ), - ), - child: Obx( - () { - final duration = widget.controller.duration.value.inMilliseconds; - int position = widget.controller.position.value.inMilliseconds; - if (_isSliderDraging) { - position = _position.inMilliseconds; - } - - return Slider( - min: 0, - max: duration.toDouble(), - value: clampDouble( - position.toDouble(), - 0, - duration.toDouble(), - ), - secondaryTrackValue: clampDouble( - _buffer.inMilliseconds.toDouble(), - 0, - duration.toDouble(), - ), - onChanged: (value) { - if (_isSliderDraging) { - setState(() { - _position = Duration(milliseconds: value.toInt()); - }); - } - }, - onChangeStart: (value) { - _position = Duration(milliseconds: value.toInt()); - _isSliderDraging = true; - }, - onChangeEnd: (value) { - if (_isSliderDraging) { - widget.controller.seek( - Duration(milliseconds: value.toInt()), - ); - _isSliderDraging = false; - } - }, - ); - }, - ), - ), - ); - } -} diff --git a/lib/views/pages/watch/video/video_player_sidebar.dart b/lib/views/pages/watch/video/video_player_sidebar.dart deleted file mode 100644 index 9e629642..00000000 --- a/lib/views/pages/watch/video/video_player_sidebar.dart +++ /dev/null @@ -1,940 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluent_ui/fluent_ui.dart' as fluent; -import 'package:get/get.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:miru_app/controllers/watch/video_controller.dart'; -import 'package:miru_app/utils/color.dart'; -import 'package:miru_app/utils/i18n.dart'; -import 'package:miru_app/views/widgets/list_title.dart'; -import 'package:miru_app/views/widgets/platform_widget.dart'; -import 'package:miru_app/views/widgets/watch/playlist.dart'; - -enum SidebarTab { - episodes, - qualitys, - torrentFiles, - tracks, - settings, -} - -_sidebarTabToString(SidebarTab tab) { - return "video.sidebar.tab.${tab.name}".i18n; -} - -class VideoPlayerSidebar extends StatefulWidget { - const VideoPlayerSidebar({ - super.key, - required this.controller, - }); - final VideoPlayerController controller; - - @override - State createState() => _VideoPlayerSidebarState(); -} - -class _VideoPlayerSidebarState extends State { - late final _c = widget.controller; - - late final Map _tabs = { - SidebarTab.episodes: PlayList( - title: _c.title, - list: _c.playList.map((e) => e.name).toList(), - selectIndex: _c.index.value, - onChange: (value) { - _c.index.value = value; - _c.showSidebar.value = false; - }, - ), - }; - - Widget _buildAndroid(BuildContext context) { - return Container( - color: ThemeData.dark().colorScheme.background, - child: DefaultTabController( - length: _tabs.length, - initialIndex: !_tabs.keys.toList().contains(_c.initSidebarTab.value) - ? 0 - : _tabs.keys.toList().indexOf(_c.initSidebarTab.value), - child: Column( - children: [ - TabBar( - tabAlignment: TabAlignment.center, - isScrollable: true, - tabs: _tabs.keys - .map((e) => Tab(text: _sidebarTabToString(e))) - .toList(), - ), - Expanded( - child: TabBarView( - children: _tabs.values.toList(), - ), - ), - ], - ), - ), - ); - } - - Widget _buildDesktop(BuildContext context) { - return fluent.FluentTheme( - data: fluent.FluentThemeData( - brightness: Brightness.dark, - ), - child: Container( - color: fluent.FluentThemeData.dark().micaBackgroundColor, - child: ListView( - padding: const EdgeInsets.all(10), - children: [ - Row( - children: [ - Text( - "common.settings".i18n, - style: fluent.FluentThemeData.dark().typography.bodyLarge, - ), - const Spacer(), - fluent.IconButton( - onPressed: () { - _c.showSidebar.value = false; - }, - icon: const Icon(fluent.FluentIcons.chrome_close), - ), - ], - ), - const SizedBox(height: 20), - _tabs[SidebarTab.settings]! - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - if (_c.torrentMediaFileList.isNotEmpty) { - _tabs.addAll( - { - SidebarTab.torrentFiles: _TorrentFiles( - controller: _c, - ), - }, - ); - } - - if (_c.qualityMap.isNotEmpty) { - _tabs.addAll( - { - SidebarTab.qualitys: _QualitySelector( - controller: _c, - ), - }, - ); - } - - _tabs.addAll( - { - SidebarTab.tracks: _TrackSelector( - controller: _c, - ), - SidebarTab.settings: _SideBarSettings( - controller: _c, - ), - }, - ); - return PlatformBuildWidget( - androidBuilder: _buildAndroid, - desktopBuilder: _buildDesktop, - ); - } -} - -class _SideBarSettings extends StatefulWidget { - const _SideBarSettings({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - State<_SideBarSettings> createState() => _SideBarSettingsState(); -} - -class _SideBarSettingsState extends State<_SideBarSettings> { - late final _c = widget.controller; - - Widget _buildDesktop(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - fluent.Card( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('video.sidebar.subtitle.title'.i18n), - const SizedBox(height: 10), - Row( - children: [ - Text('video.sidebar.subtitle.font-size'.i18n), - const SizedBox(width: 10), - Expanded( - child: Obx( - () => fluent.Slider( - value: _c.subtitleFontSize.value, - onChanged: (value) { - _c.subtitleFontSize.value = value; - }, - min: 20, - max: 80, - ), - ), - ), - const SizedBox(width: 10), - Obx( - () => Text( - _c.subtitleFontSize.value.toStringAsFixed(0), - ), - ), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - Text('video.sidebar.subtitle.font-color'.i18n), - const SizedBox(width: 10), - fluent.SplitButton( - flyout: fluent.FlyoutContent( - constraints: const BoxConstraints(maxWidth: 200.0), - child: Obx( - () => Wrap( - runSpacing: 10.0, - spacing: 8.0, - children: [ - ...ColorUtils.baseColors.map((color) { - return fluent.Button( - autofocus: _c.subtitleFontColor.value == color, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleFontColor.value = color; - Navigator.of(context).pop(color); - }, - child: Container( - height: 32, - width: 32, - color: color, - ), - ); - }), - ], - ), - ), - ), - child: Obx( - () => Container( - decoration: BoxDecoration( - color: _c.subtitleFontColor.value, - borderRadius: - const BorderRadiusDirectional.horizontal( - start: Radius.circular(4.0), - ), - ), - height: 32, - width: 36, - ), - ), - ), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - Text('video.sidebar.subtitle.background-color'.i18n), - const SizedBox(width: 10), - fluent.SplitButton( - flyout: fluent.FlyoutContent( - constraints: const BoxConstraints(maxWidth: 200.0), - child: Obx( - () => Wrap( - runSpacing: 10.0, - spacing: 8.0, - children: [ - ...ColorUtils.baseColors.map((color) { - return fluent.Button( - autofocus: - _c.subtitleBackgroundColor.value == color, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleBackgroundColor.value = color; - Navigator.of(context).pop(color); - }, - child: Container( - height: 32, - width: 32, - color: color, - ), - ); - }), - ], - ), - ), - ), - child: Obx( - () => Container( - decoration: BoxDecoration( - color: _c.subtitleBackgroundColor.value, - borderRadius: - const BorderRadiusDirectional.horizontal( - start: Radius.circular(4.0), - ), - ), - height: 32, - width: 36, - ), - ), - ), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - Text('video.sidebar.subtitle.background-opacity'.i18n), - const SizedBox(width: 10), - Expanded( - child: Obx( - () => fluent.Slider( - value: _c.subtitleBackgroundOpacity.value, - onChanged: (value) { - _c.subtitleBackgroundOpacity.value = value; - }, - min: 0, - max: 1, - ), - ), - ), - const SizedBox(width: 10), - Obx( - () => Text( - _c.subtitleBackgroundOpacity.value.toStringAsFixed(2), - ), - ), - ], - ), - const SizedBox(height: 10), - // textAlign - Row( - children: [ - Text('video.sidebar.subtitle.text-align'.i18n), - const SizedBox(width: 10), - fluent.SplitButton( - flyout: fluent.FlyoutContent( - constraints: const BoxConstraints(maxWidth: 200.0), - child: Obx( - () => Wrap( - runSpacing: 10.0, - spacing: 8.0, - children: [ - fluent.Button( - autofocus: _c.subtitleTextAlign.value == - TextAlign.justify, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleTextAlign.value = TextAlign.justify; - Navigator.of(context).pop(TextAlign.justify); - }, - child: Container( - height: 32, - width: 32, - color: Colors.transparent, - child: const Icon( - Icons.format_align_justify, - color: Colors.white, - ), - ), - ), - fluent.Button( - autofocus: - _c.subtitleTextAlign.value == TextAlign.left, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleTextAlign.value = TextAlign.left; - Navigator.of(context).pop(TextAlign.left); - }, - child: Container( - height: 32, - width: 32, - color: Colors.transparent, - child: const Icon( - Icons.format_align_left, - color: Colors.white, - ), - ), - ), - fluent.Button( - autofocus: - _c.subtitleTextAlign.value == TextAlign.right, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleTextAlign.value = TextAlign.right; - Navigator.of(context).pop(TextAlign.right); - }, - child: Container( - height: 32, - width: 32, - color: Colors.transparent, - child: const Icon( - Icons.format_align_right, - color: Colors.white, - ), - ), - ), - fluent.Button( - autofocus: _c.subtitleTextAlign.value == - TextAlign.center, - style: fluent.ButtonStyle( - padding: fluent.ButtonState.all( - const EdgeInsets.all(4.0), - ), - ), - onPressed: () { - _c.subtitleTextAlign.value = TextAlign.center; - Navigator.of(context).pop(TextAlign.center); - }, - child: Container( - height: 32, - width: 32, - color: Colors.transparent, - child: const Icon( - Icons.format_align_center, - color: Colors.white, - ), - ), - ), - ], - ), - ), - ), - child: Obx( - () => SizedBox( - height: 32, - width: 36, - child: Icon( - _c.subtitleTextAlign.value == TextAlign.justify - ? Icons.format_align_justify - : _c.subtitleTextAlign.value == TextAlign.left - ? Icons.format_align_left - : _c.subtitleTextAlign.value == - TextAlign.right - ? Icons.format_align_right - : Icons.format_align_center, - color: Colors.white, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 10), - Text('video.sidebar.subtitle.font-weight'.i18n), - const SizedBox(height: 10), - Obx( - () => Wrap( - spacing: 5, - runSpacing: 5, - children: [ - fluent.ToggleButton( - checked: _c.subtitleFontWeight.value == FontWeight.normal, - onChanged: (value) { - _c.subtitleFontWeight.value = FontWeight.normal; - }, - child: Text( - 'video.sidebar.subtitle.font-weight-normal'.i18n, - ), - ), - fluent.ToggleButton( - checked: _c.subtitleFontWeight.value == FontWeight.bold, - onChanged: (value) { - _c.subtitleFontWeight.value = FontWeight.bold; - }, - child: Text( - 'video.sidebar.subtitle.font-weight-bold'.i18n, - ), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 20), - fluent.Card( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('video.sidebar.play-mode.title'.i18n), - const SizedBox( - height: 10, - width: double.infinity, - ), - Obx( - () => Wrap( - spacing: 5, - runSpacing: 5, - children: [ - fluent.ToggleButton( - checked: _c.playMode.value == PlaylistMode.loop, - onChanged: (value) { - _c.playMode.value = PlaylistMode.loop; - }, - child: Text('video.sidebar.play-mode.loop'.i18n), - ), - fluent.ToggleButton( - checked: _c.playMode.value == PlaylistMode.single, - onChanged: (value) { - _c.playMode.value = PlaylistMode.single; - }, - child: Text('video.sidebar.play-mode.single'.i18n), - ), - fluent.ToggleButton( - checked: _c.playMode.value == PlaylistMode.none, - onChanged: (value) { - _c.playMode.value = PlaylistMode.none; - }, - child: Text('video.sidebar.play-mode.auto-next'.i18n), - ), - ], - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildAndroid(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - children: [ - Text( - 'video.sidebar.subtitle.title'.i18n, - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - const SizedBox(height: 20), - Text('video.sidebar.subtitle.font-size'.i18n), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: Obx( - () => SliderTheme( - data: SliderThemeData( - overlayShape: SliderComponentShape.noOverlay, - ), - child: Slider( - value: _c.subtitleFontSize.value, - onChanged: (value) { - _c.subtitleFontSize.value = value; - }, - min: 20, - max: 80, - ), - ), - ), - ), - Obx( - () => Text( - _c.subtitleFontSize.value.toStringAsFixed(0), - ), - ), - ], - ), - const SizedBox(height: 10), - Text('video.sidebar.subtitle.font-color'.i18n), - const SizedBox(height: 10), - Obx( - () { - final selectColor = _c.subtitleFontColor.value; - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (final color in ColorUtils.baseColors) ...[ - GestureDetector( - onTap: () { - _c.subtitleFontColor.value = color; - }, - child: Container( - decoration: BoxDecoration( - border: selectColor == color - ? Border.all( - color: Colors.grey, - width: 2, - ) - : null, - color: color, - borderRadius: const BorderRadius.all( - Radius.circular(16), - ), - ), - height: 32, - width: 32, - ), - ), - const SizedBox(width: 10) - ], - ], - ), - ); - }, - ), - const SizedBox(height: 10), - Text('video.sidebar.subtitle.background-color'.i18n), - const SizedBox(height: 10), - Obx( - () { - final selectColor = _c.subtitleBackgroundColor.value; - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (final color in ColorUtils.baseColors) ...[ - GestureDetector( - onTap: () { - _c.subtitleBackgroundColor.value = color; - }, - child: Container( - decoration: BoxDecoration( - border: selectColor == color - ? Border.all( - color: Colors.grey, - width: 2, - ) - : null, - color: color, - borderRadius: const BorderRadius.all( - Radius.circular(16), - ), - ), - height: 32, - width: 32, - ), - ), - const SizedBox(width: 10) - ], - ], - ), - ); - }, - ), - const SizedBox(height: 10), - Text( - 'video.sidebar.subtitle.background-opacity'.i18n, - ), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: Obx( - () => SliderTheme( - data: SliderThemeData( - overlayShape: SliderComponentShape.noOverlay, - ), - child: Slider( - value: _c.subtitleBackgroundOpacity.value, - onChanged: (value) { - _c.subtitleBackgroundOpacity.value = value; - }, - min: 0, - max: 1, - ), - ), - ), - ), - Obx( - () => Text( - _c.subtitleBackgroundOpacity.value.toStringAsFixed(2), - ), - ), - ], - ), - const SizedBox(height: 10), - // textAlign - Text('video.sidebar.subtitle.text-align'.i18n), - const SizedBox(height: 10), - - Obx( - () => Wrap( - children: [ - for (final align in TextAlign.values) ...[ - GestureDetector( - onTap: () { - _c.subtitleTextAlign.value = align; - }, - child: Container( - decoration: BoxDecoration( - border: _c.subtitleTextAlign.value == align - ? Border.all( - color: Colors.grey, - width: 2, - ) - : null, - color: Colors.transparent, - borderRadius: const BorderRadius.all( - Radius.circular(5), - ), - ), - height: 32, - width: 32, - child: Icon( - align == TextAlign.justify - ? Icons.format_align_justify - : align == TextAlign.left - ? Icons.format_align_left - : align == TextAlign.right - ? Icons.format_align_right - : Icons.format_align_center, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 10) - ], - ], - ), - ), - const SizedBox(height: 10), - Text( - 'video.sidebar.subtitle.font-weight'.i18n, - ), - const SizedBox(height: 10), - Obx( - () => SegmentedButton( - showSelectedIcon: false, - segments: [ - ButtonSegment( - value: FontWeight.normal, - label: Text( - 'video.sidebar.subtitle.font-weight-normal'.i18n, - ), - ), - ButtonSegment( - value: FontWeight.bold, - label: Text( - 'video.sidebar.subtitle.font-weight-bold'.i18n, - ), - ), - ], - selected: {_c.subtitleFontWeight.value}, - onSelectionChanged: (value) { - _c.subtitleFontWeight.value = value.first; - }, - ), - ), - const SizedBox(height: 20), - Text( - 'video.sidebar.play-mode.title'.i18n, - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - const SizedBox(height: 10), - Obx( - () => SegmentedButton( - showSelectedIcon: false, - segments: [ - ButtonSegment( - value: PlaylistMode.loop, - label: Text( - 'video.sidebar.play-mode.loop'.i18n, - ), - ), - ButtonSegment( - value: PlaylistMode.single, - label: Text( - 'video.sidebar.play-mode.single'.i18n, - ), - ), - ButtonSegment( - value: PlaylistMode.none, - label: Text( - 'video.sidebar.play-mode.auto-next'.i18n, - ), - ), - ], - selected: {_c.playMode.value}, - onSelectionChanged: (value) { - _c.playMode.value = value.first; - }, - ), - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return PlatformBuildWidget( - androidBuilder: _buildAndroid, - desktopBuilder: _buildDesktop, - ); - } -} - -class _QualitySelector extends StatefulWidget { - const _QualitySelector({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - State<_QualitySelector> createState() => _QualitySelectorState(); -} - -class _QualitySelectorState extends State<_QualitySelector> { - late final _c = widget.controller; - - @override - Widget build(BuildContext context) { - return ListView( - children: [ - for (final quality in _c.qualityMap.entries) - ListTile( - onTap: () { - _c.switchQuality(quality.value); - _c.showSidebar.value = false; - }, - title: Text( - quality.key, - ), - ), - ], - ); - } -} - -class _TrackSelector extends StatelessWidget { - const _TrackSelector({ - required this.controller, - }); - final VideoPlayerController controller; - - @override - Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 10), - children: [ - ListTitle( - title: 'video.subtitle'.i18n, - ), - ListTile( - selected: - SubtitleTrack.no() == controller.player.state.track.subtitle, - title: Text('common.off'.i18n), - onTap: () { - controller.setSubtitleTrack( - SubtitleTrack.no(), - ); - controller.showSidebar.value = false; - }, - ), - ListTile( - title: Text('video.subtitle-file'.i18n), - onTap: () { - controller.addSubtitleFile(); - controller.showSidebar.value = false; - }, - ), - // 来自扩展的字幕 - for (final subtitle in controller.subtitles) - ListTile( - selected: subtitle == controller.player.state.track.subtitle, - title: Text(subtitle.title ?? ''), - subtitle: Text(subtitle.language ?? ''), - onTap: () { - controller.setSubtitleTrack( - subtitle, - ); - controller.showSidebar.value = false; - }, - ), - // 来自视频本身的字幕 - for (final subtitle in controller.player.state.tracks.subtitle) - if (subtitle != SubtitleTrack.no() && - (subtitle.language != null || subtitle.title != null)) - ListTile( - selected: subtitle == controller.player.state.track.subtitle, - title: Text(subtitle.title ?? ''), - subtitle: Text(subtitle.language ?? ''), - onTap: () { - controller.setSubtitleTrack( - subtitle, - ); - controller.showSidebar.value = false; - }, - ), - const SizedBox(height: 10), - ListTitle( - title: 'video.audio'.i18n, - ), - const SizedBox(height: 5), - for (final audio in controller.player.state.tracks.audio) - if (audio.language != null || audio.title != null) - ListTile( - selected: audio == controller.player.state.track.audio, - title: Text(audio.title ?? ''), - subtitle: Text(audio.language ?? ''), - onTap: () { - controller.player.setAudioTrack( - audio, - ); - controller.showSidebar.value = false; - }, - ), - ], - ); - } -} - -class _TorrentFiles extends StatelessWidget { - const _TorrentFiles({required this.controller}); - final VideoPlayerController controller; - - @override - Widget build(BuildContext context) { - return ListView( - children: [ - for (final file in controller.torrentMediaFileList) - ListTile( - selected: controller.currentTorrentFile.value == file, - title: Text( - file, - style: const TextStyle( - fontSize: 13, - ), - ), - onTap: () { - controller.playTorrentFile(file); - controller.showSidebar.value = false; - }, - ), - ], - ); - } -}